activerecord-import 0.25.0 → 1.7.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 (141) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +151 -0
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +74 -8
  5. data/.rubocop_todo.yml +10 -16
  6. data/Brewfile +3 -1
  7. data/CHANGELOG.md +232 -2
  8. data/Dockerfile +23 -0
  9. data/Gemfile +26 -14
  10. data/LICENSE +21 -56
  11. data/README.markdown +612 -21
  12. data/Rakefile +4 -1
  13. data/activerecord-import.gemspec +6 -5
  14. data/benchmarks/benchmark.rb +10 -4
  15. data/benchmarks/lib/base.rb +4 -2
  16. data/benchmarks/lib/cli_parser.rb +4 -2
  17. data/benchmarks/lib/float.rb +2 -0
  18. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  19. data/benchmarks/lib/output_to_csv.rb +2 -0
  20. data/benchmarks/lib/output_to_html.rb +4 -2
  21. data/benchmarks/models/test_innodb.rb +2 -0
  22. data/benchmarks/models/test_memory.rb +2 -0
  23. data/benchmarks/models/test_myisam.rb +2 -0
  24. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  25. data/docker-compose.yml +34 -0
  26. data/gemfiles/4.2.gemfile +2 -0
  27. data/gemfiles/5.0.gemfile +2 -0
  28. data/gemfiles/5.1.gemfile +2 -0
  29. data/gemfiles/5.2.gemfile +2 -0
  30. data/gemfiles/6.0.gemfile +4 -0
  31. data/gemfiles/6.1.gemfile +4 -0
  32. data/gemfiles/7.0.gemfile +4 -0
  33. data/gemfiles/7.1.gemfile +3 -0
  34. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  36. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  38. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  39. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  40. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  41. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  42. data/lib/activerecord-import/active_record/adapters/trilogy_adapter.rb +8 -0
  43. data/lib/activerecord-import/adapters/abstract_adapter.rb +15 -6
  44. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  45. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  46. data/lib/activerecord-import/adapters/mysql_adapter.rb +34 -29
  47. data/lib/activerecord-import/adapters/postgresql_adapter.rb +74 -55
  48. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +138 -13
  49. data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
  50. data/lib/activerecord-import/base.rb +11 -2
  51. data/lib/activerecord-import/import.rb +290 -114
  52. data/lib/activerecord-import/mysql2.rb +2 -0
  53. data/lib/activerecord-import/postgresql.rb +2 -0
  54. data/lib/activerecord-import/sqlite3.rb +2 -0
  55. data/lib/activerecord-import/synchronize.rb +4 -2
  56. data/lib/activerecord-import/value_sets_parser.rb +5 -0
  57. data/lib/activerecord-import/version.rb +3 -1
  58. data/lib/activerecord-import.rb +2 -1
  59. data/test/adapters/jdbcmysql.rb +2 -0
  60. data/test/adapters/jdbcpostgresql.rb +2 -0
  61. data/test/adapters/jdbcsqlite3.rb +2 -0
  62. data/test/adapters/makara_postgis.rb +2 -0
  63. data/test/adapters/mysql2.rb +2 -0
  64. data/test/adapters/mysql2_makara.rb +2 -0
  65. data/test/adapters/mysql2spatial.rb +2 -0
  66. data/test/adapters/postgis.rb +2 -0
  67. data/test/adapters/postgresql.rb +2 -0
  68. data/test/adapters/postgresql_makara.rb +2 -0
  69. data/test/adapters/seamless_database_pool.rb +2 -0
  70. data/test/adapters/spatialite.rb +2 -0
  71. data/test/adapters/sqlite3.rb +2 -0
  72. data/test/adapters/trilogy.rb +9 -0
  73. data/test/database.yml.sample +7 -0
  74. data/test/{travis → github}/database.yml +7 -1
  75. data/test/import_test.rb +151 -8
  76. data/test/jdbcmysql/import_test.rb +5 -3
  77. data/test/jdbcpostgresql/import_test.rb +4 -2
  78. data/test/jdbcsqlite3/import_test.rb +4 -2
  79. data/test/makara_postgis/import_test.rb +4 -2
  80. data/test/models/account.rb +2 -0
  81. data/test/models/alarm.rb +2 -0
  82. data/test/models/animal.rb +8 -0
  83. data/test/models/author.rb +7 -0
  84. data/test/models/bike_maker.rb +3 -0
  85. data/test/models/book.rb +7 -2
  86. data/test/models/car.rb +2 -0
  87. data/test/models/card.rb +5 -0
  88. data/test/models/chapter.rb +2 -0
  89. data/test/models/composite_book.rb +19 -0
  90. data/test/models/composite_chapter.rb +9 -0
  91. data/test/models/customer.rb +18 -0
  92. data/test/models/deck.rb +8 -0
  93. data/test/models/dictionary.rb +2 -0
  94. data/test/models/discount.rb +2 -0
  95. data/test/models/end_note.rb +2 -0
  96. data/test/models/group.rb +2 -0
  97. data/test/models/order.rb +17 -0
  98. data/test/models/playing_card.rb +4 -0
  99. data/test/models/promotion.rb +2 -0
  100. data/test/models/question.rb +2 -0
  101. data/test/models/rule.rb +2 -0
  102. data/test/models/tag.rb +9 -1
  103. data/test/models/tag_alias.rb +11 -0
  104. data/test/models/topic.rb +7 -0
  105. data/test/models/user.rb +2 -0
  106. data/test/models/user_token.rb +3 -0
  107. data/test/models/vendor.rb +2 -0
  108. data/test/models/widget.rb +2 -0
  109. data/test/mysql2/import_test.rb +5 -3
  110. data/test/mysql2_makara/import_test.rb +5 -3
  111. data/test/mysqlspatial2/import_test.rb +5 -3
  112. data/test/postgis/import_test.rb +4 -2
  113. data/test/postgresql/import_test.rb +4 -2
  114. data/test/schema/generic_schema.rb +37 -1
  115. data/test/schema/jdbcpostgresql_schema.rb +3 -1
  116. data/test/schema/mysql2_schema.rb +2 -0
  117. data/test/schema/postgis_schema.rb +3 -1
  118. data/test/schema/postgresql_schema.rb +49 -0
  119. data/test/schema/sqlite3_schema.rb +15 -0
  120. data/test/schema/version.rb +2 -0
  121. data/test/sqlite3/import_test.rb +4 -2
  122. data/test/support/active_support/test_case_extensions.rb +2 -0
  123. data/test/support/assertions.rb +2 -0
  124. data/test/support/factories.rb +10 -8
  125. data/test/support/generate.rb +10 -8
  126. data/test/support/mysql/import_examples.rb +2 -1
  127. data/test/support/postgresql/import_examples.rb +152 -3
  128. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  129. data/test/support/shared_examples/on_duplicate_key_update.rb +122 -9
  130. data/test/support/shared_examples/recursive_import.rb +128 -2
  131. data/test/support/sqlite3/import_examples.rb +191 -26
  132. data/test/synchronize_test.rb +2 -0
  133. data/test/test_helper.rb +34 -7
  134. data/test/trilogy/import_test.rb +7 -0
  135. data/test/value_sets_bytes_parser_test.rb +3 -1
  136. data/test/value_sets_records_parser_test.rb +3 -1
  137. metadata +46 -16
  138. data/.travis.yml +0 -71
  139. data/gemfiles/3.2.gemfile +0 -2
  140. data/gemfiles/4.0.gemfile +0 -2
  141. data/gemfiles/4.1.gemfile +0 -2
@@ -1,4 +1,5 @@
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
 
@@ -37,6 +38,12 @@ def should_support_postgresql_import_functionality
37
38
  assert !topic.changed?
38
39
  end
39
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
+
40
47
  it "marks models as persisted" do
41
48
  assert !topic.new_record?
42
49
  assert topic.persisted?
@@ -96,6 +103,8 @@ def should_support_postgresql_import_functionality
96
103
  books.first.id.to_s
97
104
  end
98
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' }
99
108
 
100
109
  it "creates records" do
101
110
  assert_difference("Book.count", +1) { result }
@@ -110,6 +119,26 @@ def should_support_postgresql_import_functionality
110
119
  assert_equal [%w(King It)], result.results
111
120
  end
112
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
+
113
142
  context "when primary key and returning overlap" do
114
143
  let(:result) { Book.import(books, returning: %w(id title)) }
115
144
 
@@ -124,6 +153,34 @@ def should_support_postgresql_import_functionality
124
153
  end
125
154
  end
126
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
+
127
184
  context "setting model attributes" do
128
185
  let(:code) { 'abc' }
129
186
  let(:discount) { 0.10 }
@@ -153,6 +210,14 @@ def should_support_postgresql_import_functionality
153
210
  assert_equal updated_promotion.discount, discount
154
211
  end
155
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
156
221
  end
157
222
  end
158
223
  end
@@ -228,10 +293,34 @@ def should_support_postgresql_import_functionality
228
293
  assert_equal({}, Vendor.first.json_data)
229
294
  end
230
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
231
320
  end
232
321
 
233
322
  describe "with binary field" do
234
- let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".force_encoding('ASCII-8BIT') }
323
+ let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".dup.force_encoding('ASCII-8BIT') }
235
324
  it "imports the correct values for binary fields" do
236
325
  alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
237
326
  assert_difference "Alarm.count", +1 do
@@ -240,6 +329,42 @@ def should_support_postgresql_import_functionality
240
329
  assert_equal(binary_value, Alarm.first.secret_key)
241
330
  end
242
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
+
355
+ it "should import models with auto-incrementing ID successfully" do
356
+ author = Author.create!(name: "Foo Barson")
357
+
358
+ books = []
359
+ 2.times do |i|
360
+ books << CompositeBook.new(author_id: author.id, title: "book #{i}")
361
+ end
362
+ assert_difference "CompositeBook.count", +2 do
363
+ CompositeBook.import books
364
+ end
365
+ end
366
+ end
367
+ end
243
368
  end
244
369
 
245
370
  def should_support_postgresql_upsert_functionality
@@ -295,6 +420,30 @@ def should_support_postgresql_upsert_functionality
295
420
  end
296
421
 
297
422
  context "using a hash" do
423
+ context "with :columns :all" do
424
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
425
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
426
+
427
+ macro(:perform_import) do |*opts|
428
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
429
+ end
430
+
431
+ setup do
432
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
433
+ Topic.import columns + ['replies_count'], values, validate: false
434
+ end
435
+
436
+ it "should update all specified columns" do
437
+ perform_import
438
+ updated_topic = Topic.find(99)
439
+ assert_equal 'Book - 2nd Edition', updated_topic.title
440
+ assert_equal 'Jane Doe', updated_topic.author_name
441
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
442
+ assert_equal 57, updated_topic.parent_id
443
+ assert_equal 3, updated_topic.replies_count
444
+ end
445
+ end
446
+
298
447
  context "with :columns a hash" do
299
448
  let(:columns) { %w( id title author_name author_email_address parent_id ) }
300
449
  let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
@@ -312,7 +461,7 @@ def should_support_postgresql_upsert_functionality
312
461
  it "should not modify the passed in :on_duplicate_key_update columns array" do
313
462
  assert_nothing_raised do
314
463
  columns = %w(title author_name).freeze
315
- Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: { columns: columns }
464
+ Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
316
465
  end
317
466
  end
318
467
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  def should_support_on_duplicate_key_ignore
2
4
  describe "#import" do
3
5
  extend ActiveSupport::TestCase::ImportAssertions
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  def should_support_basic_on_duplicate_key_update
2
4
  describe "#import" do
3
5
  extend ActiveSupport::TestCase::ImportAssertions
@@ -24,7 +26,7 @@ def should_support_basic_on_duplicate_key_update
24
26
  User.import(updated_users, on_duplicate_key_update: [:name])
25
27
  assert User.count == updated_users.length
26
28
  User.all.each_with_index do |user, i|
27
- assert_equal user.name, users[i].name + ' Rothschild'
29
+ assert_equal user.name, "#{users[i].name} Rothschild"
28
30
  assert_equal 1, user.lock_version
29
31
  end
30
32
  end
@@ -48,7 +50,7 @@ def should_support_basic_on_duplicate_key_update
48
50
  User.import(columns, updated_values, on_duplicate_key_update: [:name])
49
51
  assert User.count == updated_values.length
50
52
  User.all.each_with_index do |user, i|
51
- assert_equal user.name, users[i].name + ' Rothschild'
53
+ assert_equal user.name, "#{users[i].name} Rothschild"
52
54
  assert_equal 1, user.lock_version
53
55
  end
54
56
  end
@@ -70,9 +72,19 @@ def should_support_basic_on_duplicate_key_update
70
72
  User.import(updated_values, on_duplicate_key_update: [:name])
71
73
  assert User.count == updated_values.length
72
74
  User.all.each_with_index do |user, i|
73
- assert_equal user.name, users[i].name + ' Rothschild'
75
+ assert_equal user.name, "#{users[i].name} Rothschild"
74
76
  assert_equal 1, user.lock_version
75
77
  end
78
+ updated_values2 = User.all.map do |user|
79
+ user.name += ' jr.'
80
+ { id: user.id, name: user.name }
81
+ end
82
+ User.import(updated_values2, on_duplicate_key_update: [:name])
83
+ assert User.count == updated_values2.length
84
+ User.all.each_with_index do |user, i|
85
+ assert_equal user.name, "#{users[i].name} Rothschild jr."
86
+ assert_equal 2, user.lock_version
87
+ end
76
88
  end
77
89
 
78
90
  it 'upsert optimistic lock columns other than lock_version by model' do
@@ -92,7 +104,7 @@ def should_support_basic_on_duplicate_key_update
92
104
  Account.import(updated_accounts, on_duplicate_key_update: [:id, :name])
93
105
  assert Account.count == updated_accounts.length
94
106
  Account.all.each_with_index do |user, i|
95
- assert_equal user.name, accounts[i].name + ' Rothschild'
107
+ assert_equal user.name, "#{accounts[i].name} Rothschild"
96
108
  assert_equal 1, user.lock
97
109
  end
98
110
  end
@@ -116,7 +128,7 @@ def should_support_basic_on_duplicate_key_update
116
128
  Account.import(columns, updated_values, on_duplicate_key_update: [:name])
117
129
  assert Account.count == updated_values.length
118
130
  Account.all.each_with_index do |user, i|
119
- assert_equal user.name, accounts[i].name + ' Rothschild'
131
+ assert_equal user.name, "#{accounts[i].name} Rothschild"
120
132
  assert_equal 1, user.lock
121
133
  end
122
134
  end
@@ -138,7 +150,7 @@ def should_support_basic_on_duplicate_key_update
138
150
  Account.import(updated_values, on_duplicate_key_update: [:name])
139
151
  assert Account.count == updated_values.length
140
152
  Account.all.each_with_index do |user, i|
141
- assert_equal user.name, accounts[i].name + ' Rothschild'
153
+ assert_equal user.name, "#{accounts[i].name} Rothschild"
142
154
  assert_equal 1, user.lock
143
155
  end
144
156
  end
@@ -160,10 +172,11 @@ def should_support_basic_on_duplicate_key_update
160
172
  Bike::Maker.import(updated_makers, on_duplicate_key_update: [:name])
161
173
  assert Bike::Maker.count == updated_makers.length
162
174
  Bike::Maker.all.each_with_index do |maker, i|
163
- assert_equal maker.name, makers[i].name + ' bikes'
175
+ assert_equal maker.name, "#{makers[i].name} bikes"
164
176
  assert_equal 1, maker.lock_version
165
177
  end
166
178
  end
179
+
167
180
  it 'update the lock_version of models separated by namespaces by array' do
168
181
  makers = [
169
182
  Bike::Maker.new(name: 'Yamaha'),
@@ -183,7 +196,7 @@ def should_support_basic_on_duplicate_key_update
183
196
  Bike::Maker.import(columns, updated_values, on_duplicate_key_update: [:name])
184
197
  assert Bike::Maker.count == updated_values.length
185
198
  Bike::Maker.all.each_with_index do |maker, i|
186
- assert_equal maker.name, makers[i].name + ' bikes'
199
+ assert_equal maker.name, "#{makers[i].name} bikes"
187
200
  assert_equal 1, maker.lock_version
188
201
  end
189
202
  end
@@ -205,14 +218,66 @@ def should_support_basic_on_duplicate_key_update
205
218
  Bike::Maker.import(updated_values, on_duplicate_key_update: [:name])
206
219
  assert Bike::Maker.count == updated_values.length
207
220
  Bike::Maker.all.each_with_index do |maker, i|
208
- assert_equal maker.name, makers[i].name + ' bikes'
221
+ assert_equal maker.name, "#{makers[i].name} bikes"
209
222
  assert_equal 1, maker.lock_version
210
223
  end
211
224
  end
212
225
  end
226
+
227
+ context 'with locking disabled' do
228
+ it 'does not update the lock_version' do
229
+ users = [
230
+ User.new(name: 'Salomon'),
231
+ User.new(name: 'Nathan')
232
+ ]
233
+ User.import(users)
234
+ assert User.count == users.length
235
+ User.all.each do |user|
236
+ assert_equal 0, user.lock_version
237
+ end
238
+ updated_users = User.all.map do |user|
239
+ user.name += ' Rothschild'
240
+ user
241
+ end
242
+
243
+ ActiveRecord::Base.lock_optimistically = false # Disable locking
244
+ User.import(updated_users, on_duplicate_key_update: [:name])
245
+ ActiveRecord::Base.lock_optimistically = true # Enable locking
246
+
247
+ assert User.count == updated_users.length
248
+ User.all.each_with_index do |user, i|
249
+ assert_equal user.name, "#{users[i].name} Rothschild"
250
+ assert_equal 0, user.lock_version
251
+ end
252
+ end
253
+ end
213
254
  end
214
255
 
215
256
  context "with :on_duplicate_key_update" do
257
+ describe 'using :all' do
258
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
259
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
260
+
261
+ macro(:perform_import) do |*opts|
262
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: :all, validate: false)
263
+ end
264
+
265
+ setup do
266
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
267
+ Topic.import columns + ['replies_count'], values, validate: false
268
+ end
269
+
270
+ it 'updates all specified columns' do
271
+ perform_import
272
+ updated_topic = Topic.find(99)
273
+ assert_equal 'Book - 2nd Edition', updated_topic.title
274
+ assert_equal 'Jane Doe', updated_topic.author_name
275
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
276
+ assert_equal 57, updated_topic.parent_id
277
+ assert_equal 3, updated_topic.replies_count
278
+ end
279
+ end
280
+
216
281
  describe "argument safety" do
217
282
  it "should not modify the passed in :on_duplicate_key_update array" do
218
283
  assert_nothing_raised do
@@ -222,6 +287,26 @@ def should_support_basic_on_duplicate_key_update
222
287
  end
223
288
  end
224
289
 
290
+ context "with timestamps enabled" do
291
+ let(:time) { Chronic.parse("5 minutes from now") }
292
+
293
+ it 'should not overwrite changed updated_at with current timestamp' do
294
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
295
+ timestamp = Time.now.utc
296
+ topic.updated_at = timestamp
297
+ Topic.import [topic], on_duplicate_key_update: :all, validate: false
298
+ assert_equal timestamp.to_s, Topic.last.updated_at.to_s
299
+ end
300
+
301
+ it 'should update updated_at with current timestamp' do
302
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
303
+ Timecop.freeze(time) do
304
+ Topic.import [topic], on_duplicate_key_update: [:updated_at], validate: false
305
+ assert_in_delta time.to_i, topic.reload.updated_at.to_i, 1.second
306
+ end
307
+ end
308
+ end
309
+
225
310
  context "with validation checks turned off" do
226
311
  asssertion_group(:should_support_on_duplicate_key_update) do
227
312
  should_not_update_fields_not_mentioned
@@ -260,6 +345,34 @@ def should_support_basic_on_duplicate_key_update
260
345
  should_support_on_duplicate_key_update
261
346
  should_update_fields_mentioned
262
347
  end
348
+
349
+ context "using column aliases" do
350
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
351
+ let(:update_columns) { %w(title author_email_address parent_id) }
352
+
353
+ context "with column aliases in column list" do
354
+ let(:columns) { %w( id name author_name author_email_address parent_id ) }
355
+ should_support_on_duplicate_key_update
356
+ should_update_fields_mentioned
357
+ end
358
+
359
+ context "with column aliases in update columns list" do
360
+ let(:update_columns) { %w(name author_email_address parent_id) }
361
+ should_support_on_duplicate_key_update
362
+ should_update_fields_mentioned
363
+ end
364
+ end
365
+
366
+ if ENV['AR_VERSION'].to_i >= 6.0
367
+ context "using ignored columns" do
368
+ let(:columns) { %w( id title author_name author_email_address parent_id priority ) }
369
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17, 1]] }
370
+ let(:update_columns) { %w(name author_email_address parent_id priority) }
371
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57, 2]] }
372
+ should_support_on_duplicate_key_update
373
+ should_update_fields_mentioned
374
+ end
375
+ end
263
376
  end
264
377
 
265
378
  context "with a table that has a non-standard primary key" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  def should_support_recursive_import
2
4
  describe "importing objects with associations" do
3
5
  let(:new_topics) { Build(num_topics, :topic_with_book) }
@@ -11,7 +13,7 @@ def should_support_recursive_import
11
13
  let(:num_chapters) { 18 }
12
14
  let(:num_endnotes) { 24 }
13
15
 
14
- let(:new_question_with_rule) { FactoryGirl.build :question, :with_rule }
16
+ let(:new_question_with_rule) { FactoryBot.build :question, :with_rule }
15
17
 
16
18
  it 'imports top level' do
17
19
  assert_difference "Topic.count", +num_topics do
@@ -138,6 +140,15 @@ def should_support_recursive_import
138
140
  books.each do |book|
139
141
  assert_equal book.topic_id, second_new_topic.id
140
142
  end
143
+
144
+ books.each { |book| book.topic_id = nil }
145
+ assert_no_difference "Book.count", books.size do
146
+ Book.import books, validate: false, on_duplicate_key_update: [:topic_id]
147
+ end
148
+
149
+ books.each do |book|
150
+ assert_nil book.topic_id, nil
151
+ end
141
152
  end
142
153
 
143
154
  unless ENV["SKIP_COMPOSITE_PK"]
@@ -154,6 +165,24 @@ def should_support_recursive_import
154
165
  assert_equal 1, tags[0].tag_id
155
166
  assert_equal 2, tags[1].tag_id
156
167
  end
168
+
169
+ if ENV['AR_VERSION'].to_f >= 7.1
170
+ it "should import models with auto-incrementing ID successfully with recursive set to true" do
171
+ author = Author.create!(name: "Foo Barson")
172
+ books = []
173
+ 2.times do |i|
174
+ books << CompositeBook.new(author_id: author.id, title: "Book #{i}", composite_chapters: [
175
+ CompositeChapter.new(title: "Book #{i} composite chapter 1"),
176
+ CompositeChapter.new(title: "Book #{i} composite chapter 2"),
177
+ ])
178
+ end
179
+ assert_difference "CompositeBook.count", +2 do
180
+ assert_difference "CompositeChapter.count", +4 do
181
+ CompositeBook.import books, recursive: true
182
+ end
183
+ end
184
+ end
185
+ end
157
186
  end
158
187
  end
159
188
 
@@ -167,7 +196,7 @@ def should_support_recursive_import
167
196
  end
168
197
  end
169
198
 
170
- # If adapter supports on_duplicate_key_update, it is only applied to top level models so that SQL with invalid
199
+ # If adapter supports on_duplicate_key_update and specific columns are specified, it is only applied to top level models so that SQL with invalid
171
200
  # columns, keys, etc isn't generated for child associations when doing recursive import
172
201
  if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
173
202
  describe "on_duplicate_key_update" do
@@ -181,6 +210,103 @@ def should_support_recursive_import
181
210
  end
182
211
  end
183
212
  end
213
+
214
+ context "when :all fields are updated" do
215
+ setup do
216
+ Topic.import new_topics, recursive: true
217
+ end
218
+
219
+ it "updates associated objects" do
220
+ new_author_name = 'Richard Bachman'
221
+ topic = new_topics.first
222
+ topic.books.each do |book|
223
+ book.author_name = new_author_name
224
+ end
225
+ assert_nothing_raised do
226
+ Topic.import new_topics, recursive: true, on_duplicate_key_update: :all
227
+ end
228
+ Topic.find(topic.id).books.each do |book|
229
+ assert_equal new_author_name, book.author_name
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ describe "recursive_on_duplicate_key_update" do
236
+ let(:new_topics) { Build(1, :topic_with_book) }
237
+
238
+ setup do
239
+ Topic.import new_topics, recursive: true
240
+ end
241
+
242
+ it "updates associated objects" do
243
+ new_author_name = 'Richard Bachman'
244
+ topic = new_topics.first
245
+ topic.books.each do |book|
246
+ book.author_name = new_author_name
247
+ end
248
+
249
+ assert_nothing_raised do
250
+ Topic.import new_topics,
251
+ recursive: true,
252
+ on_duplicate_key_update: [:id],
253
+ recursive_on_duplicate_key_update: {
254
+ books: { conflict_target: [:id], columns: [:author_name] }
255
+ }
256
+ end
257
+ Topic.find(topic.id).books.each do |book|
258
+ assert_equal new_author_name, book.author_name
259
+ end
260
+ end
261
+
262
+ it "updates nested associated objects" do
263
+ new_chapter_title = 'The Final Chapter'
264
+ book = new_topics.first.books.first
265
+ book.author_name = 'Richard Bachman'
266
+
267
+ example_chapter = book.chapters.first
268
+ example_chapter.title = new_chapter_title
269
+
270
+ assert_nothing_raised do
271
+ Topic.import new_topics,
272
+ recursive: true,
273
+ on_duplicate_key_update: [:id],
274
+ recursive_on_duplicate_key_update: {
275
+ books: { conflict_target: [:id], columns: [:author_name] },
276
+ chapters: { conflict_target: [:id], columns: [:title] }
277
+ }
278
+ end
279
+ assert_equal new_chapter_title, Chapter.find(example_chapter.id).title
280
+ end
281
+ end
282
+ end
283
+
284
+ # If returning option is provided, it is only applied to top level models so that SQL with invalid
285
+ # columns, keys, etc isn't generated for child associations when doing recursive import
286
+ describe "returning" do
287
+ let(:new_topics) { Build(1, :topic_with_book) }
288
+
289
+ it "imports objects with associations" do
290
+ assert_difference "Topic.count", +1 do
291
+ Topic.import new_topics, recursive: true, returning: [:content], validate: false
292
+ new_topics.each do |topic|
293
+ assert_not_nil topic.id
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ # If no returning option is provided, it is ignored
300
+ describe "no returning" do
301
+ let(:new_topics) { Build(1, :topic_with_book) }
302
+
303
+ it "is ignored and imports objects with associations" do
304
+ assert_difference "Topic.count", +1 do
305
+ Topic.import new_topics, recursive: true, no_returning: true, validate: false
306
+ new_topics.each do |topic|
307
+ assert_not_nil topic.id
308
+ end
309
+ end
184
310
  end
185
311
  end
186
312
  end