activerecord-import 0.10.0 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +49 -0
  4. data/.rubocop_todo.yml +36 -0
  5. data/.travis.yml +64 -8
  6. data/CHANGELOG.md +475 -0
  7. data/Gemfile +32 -15
  8. data/LICENSE +21 -56
  9. data/README.markdown +564 -35
  10. data/Rakefile +20 -3
  11. data/activerecord-import.gemspec +7 -7
  12. data/benchmarks/README +2 -2
  13. data/benchmarks/benchmark.rb +68 -64
  14. data/benchmarks/lib/base.rb +138 -137
  15. data/benchmarks/lib/cli_parser.rb +107 -103
  16. data/benchmarks/lib/{mysql_benchmark.rb → mysql2_benchmark.rb} +19 -22
  17. data/benchmarks/lib/output_to_csv.rb +5 -4
  18. data/benchmarks/lib/output_to_html.rb +8 -13
  19. data/benchmarks/models/test_innodb.rb +1 -1
  20. data/benchmarks/models/test_memory.rb +1 -1
  21. data/benchmarks/models/test_myisam.rb +1 -1
  22. data/benchmarks/schema/mysql2_schema.rb +16 -0
  23. data/gemfiles/3.2.gemfile +2 -4
  24. data/gemfiles/4.0.gemfile +2 -4
  25. data/gemfiles/4.1.gemfile +2 -4
  26. data/gemfiles/4.2.gemfile +2 -4
  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 +2 -0
  31. data/gemfiles/6.1.gemfile +1 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +6 -0
  33. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +0 -1
  34. data/lib/activerecord-import/adapters/abstract_adapter.rb +23 -17
  35. data/lib/activerecord-import/adapters/mysql_adapter.rb +52 -25
  36. data/lib/activerecord-import/adapters/postgresql_adapter.rb +187 -10
  37. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +148 -17
  38. data/lib/activerecord-import/base.rb +15 -9
  39. data/lib/activerecord-import/import.rb +740 -191
  40. data/lib/activerecord-import/synchronize.rb +21 -21
  41. data/lib/activerecord-import/value_sets_parser.rb +33 -8
  42. data/lib/activerecord-import/version.rb +1 -1
  43. data/lib/activerecord-import.rb +4 -15
  44. data/test/adapters/jdbcsqlite3.rb +1 -0
  45. data/test/adapters/makara_postgis.rb +1 -0
  46. data/test/adapters/mysql2_makara.rb +1 -0
  47. data/test/adapters/mysql2spatial.rb +1 -1
  48. data/test/adapters/postgis.rb +1 -1
  49. data/test/adapters/postgresql.rb +1 -1
  50. data/test/adapters/postgresql_makara.rb +1 -0
  51. data/test/adapters/spatialite.rb +1 -1
  52. data/test/adapters/sqlite3.rb +1 -1
  53. data/test/database.yml.sample +13 -18
  54. data/test/import_test.rb +608 -89
  55. data/test/jdbcmysql/import_test.rb +2 -3
  56. data/test/jdbcpostgresql/import_test.rb +0 -2
  57. data/test/jdbcsqlite3/import_test.rb +4 -0
  58. data/test/makara_postgis/import_test.rb +8 -0
  59. data/test/models/account.rb +3 -0
  60. data/test/models/alarm.rb +2 -0
  61. data/test/models/animal.rb +6 -0
  62. data/test/models/bike_maker.rb +7 -0
  63. data/test/models/book.rb +7 -6
  64. data/test/models/car.rb +3 -0
  65. data/test/models/chapter.rb +2 -2
  66. data/test/models/dictionary.rb +4 -0
  67. data/test/models/discount.rb +3 -0
  68. data/test/models/end_note.rb +2 -2
  69. data/test/models/promotion.rb +3 -0
  70. data/test/models/question.rb +3 -0
  71. data/test/models/rule.rb +3 -0
  72. data/test/models/tag.rb +4 -0
  73. data/test/models/topic.rb +17 -3
  74. data/test/models/user.rb +3 -0
  75. data/test/models/user_token.rb +4 -0
  76. data/test/models/vendor.rb +7 -0
  77. data/test/models/widget.rb +19 -2
  78. data/test/mysql2/import_test.rb +2 -3
  79. data/test/{em_mysql2 → mysql2_makara}/import_test.rb +1 -1
  80. data/test/mysqlspatial2/import_test.rb +2 -2
  81. data/test/postgis/import_test.rb +5 -1
  82. data/test/schema/generic_schema.rb +159 -85
  83. data/test/schema/jdbcpostgresql_schema.rb +1 -0
  84. data/test/schema/mysql2_schema.rb +19 -0
  85. data/test/schema/postgis_schema.rb +1 -0
  86. data/test/schema/postgresql_schema.rb +61 -0
  87. data/test/schema/sqlite3_schema.rb +13 -0
  88. data/test/sqlite3/import_test.rb +2 -50
  89. data/test/support/active_support/test_case_extensions.rb +21 -13
  90. data/test/support/{mysql/assertions.rb → assertions.rb} +20 -2
  91. data/test/support/factories.rb +39 -14
  92. data/test/support/generate.rb +10 -10
  93. data/test/support/mysql/import_examples.rb +49 -98
  94. data/test/support/postgresql/import_examples.rb +535 -57
  95. data/test/support/shared_examples/on_duplicate_key_ignore.rb +43 -0
  96. data/test/support/shared_examples/on_duplicate_key_update.rb +378 -0
  97. data/test/support/shared_examples/recursive_import.rb +225 -0
  98. data/test/support/sqlite3/import_examples.rb +231 -0
  99. data/test/synchronize_test.rb +10 -2
  100. data/test/test_helper.rb +36 -8
  101. data/test/travis/database.yml +26 -17
  102. data/test/value_sets_bytes_parser_test.rb +25 -17
  103. data/test/value_sets_records_parser_test.rb +6 -6
  104. metadata +86 -42
  105. data/benchmarks/boot.rb +0 -18
  106. data/benchmarks/schema/mysql_schema.rb +0 -16
  107. data/gemfiles/3.1.gemfile +0 -4
  108. data/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb +0 -8
  109. data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +0 -6
  110. data/lib/activerecord-import/em_mysql2.rb +0 -7
  111. data/lib/activerecord-import/mysql.rb +0 -7
  112. data/test/adapters/em_mysql2.rb +0 -1
  113. data/test/adapters/mysql.rb +0 -1
  114. data/test/adapters/mysqlspatial.rb +0 -1
  115. data/test/mysql/import_test.rb +0 -6
  116. data/test/mysqlspatial/import_test.rb +0 -6
  117. data/test/schema/mysql_schema.rb +0 -18
  118. data/test/travis/build.sh +0 -30
@@ -1,5 +1,11 @@
1
1
  # encoding: UTF-8
2
2
  def should_support_postgresql_import_functionality
3
+ should_support_recursive_import
4
+
5
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
6
+ should_support_postgresql_upsert_functionality
7
+ end
8
+
3
9
  describe "#supports_imports?" do
4
10
  it "should support import" do
5
11
  assert ActiveRecord::Base.supports_import?
@@ -18,86 +24,558 @@ def should_support_postgresql_import_functionality
18
24
  end
19
25
  end
20
26
 
21
- describe "importing objects with associations" do
27
+ context "setting attributes and marking clean" do
28
+ let(:topic) { Build(:topics) }
22
29
 
23
- let(:new_topics) { Build(num_topics, :topic_with_book) }
24
- let(:new_topics_with_invalid_chapter) {
25
- chapter = new_topics.first.books.first.chapters.first
26
- chapter.title = nil
27
- new_topics
28
- }
29
- let(:num_topics) {3}
30
- let(:num_books) {6}
31
- let(:num_chapters) {18}
32
- let(:num_endnotes) {24}
30
+ setup { Topic.import([topic]) }
33
31
 
34
- it 'imports top level' do
35
- assert_difference "Topic.count", +num_topics do
36
- Topic.import new_topics, :recursive => true
37
- new_topics.each do |topic|
38
- assert_not_nil topic.id
39
- end
32
+ it "assigns ids" do
33
+ assert topic.id.present?
34
+ end
35
+
36
+ it "marks models as clean" do
37
+ assert !topic.changed?
38
+ end
39
+
40
+ if ENV['AR_VERSION'].to_f > 4.1
41
+ it "moves the dirty changes to previous_changes" do
42
+ assert topic.previous_changes.present?
40
43
  end
41
44
  end
42
45
 
43
- it 'imports first level associations' do
44
- assert_difference "Book.count", +num_books do
45
- Topic.import new_topics, :recursive => true
46
- new_topics.each do |topic|
47
- topic.books.each do |book|
48
- assert_equal topic.id, book.topic_id
49
- end
50
- end
46
+ it "marks models as persisted" do
47
+ assert !topic.new_record?
48
+ assert topic.persisted?
49
+ end
50
+
51
+ it "assigns timestamps" do
52
+ assert topic.created_at.present?
53
+ assert topic.updated_at.present?
54
+ end
55
+ end
56
+
57
+ describe "with query cache enabled" do
58
+ setup do
59
+ unless ActiveRecord::Base.connection.query_cache_enabled
60
+ ActiveRecord::Base.connection.enable_query_cache!
61
+ @disable_cache_on_teardown = true
62
+ end
63
+ end
64
+
65
+ it "clears cache on insert" do
66
+ before_import = Topic.all.to_a
67
+
68
+ Topic.import(Build(2, :topics), validate: false)
69
+
70
+ after_import = Topic.all.to_a
71
+ assert_equal 2, after_import.size - before_import.size
72
+ end
73
+
74
+ teardown do
75
+ if @disable_cache_on_teardown
76
+ ActiveRecord::Base.connection.disable_query_cache!
77
+ end
78
+ end
79
+ end
80
+
81
+ describe "no_returning" do
82
+ let(:books) { [Book.new(author_name: "foo", title: "bar")] }
83
+
84
+ it "creates records" do
85
+ assert_difference "Book.count", +1 do
86
+ Book.import books, no_returning: true
87
+ end
88
+ end
89
+
90
+ it "returns no ids" do
91
+ assert_equal [], Book.import(books, no_returning: true).ids
92
+ end
93
+ end
94
+
95
+ describe "returning" do
96
+ let(:books) { [Book.new(author_name: "King", title: "It")] }
97
+ let(:result) { Book.import(books, returning: %w(author_name title)) }
98
+ let(:book_id) do
99
+ if RUBY_PLATFORM == 'java' || ENV['AR_VERSION'].to_i >= 5.0
100
+ books.first.id
101
+ else
102
+ books.first.id.to_s
103
+ end
104
+ end
105
+
106
+ it "creates records" do
107
+ assert_difference("Book.count", +1) { result }
108
+ end
109
+
110
+ it "returns ids" do
111
+ result
112
+ assert_equal [book_id], result.ids
113
+ end
114
+
115
+ it "returns specified columns" do
116
+ assert_equal [%w(King It)], result.results
117
+ end
118
+
119
+ context "when given an empty array" do
120
+ let(:result) { Book.import([], returning: %w(title)) }
121
+
122
+ setup { result }
123
+
124
+ it "returns empty arrays for ids and results" do
125
+ assert_equal [], result.ids
126
+ assert_equal [], result.results
127
+ end
128
+ end
129
+
130
+ context "when a returning column is a serialized attribute" do
131
+ let(:vendor) { Vendor.new(hours: { monday: '8-5' }) }
132
+ let(:result) { Vendor.import([vendor], returning: %w(hours)) }
133
+
134
+ it "creates records" do
135
+ assert_difference("Vendor.count", +1) { result }
136
+ end
137
+ end
138
+
139
+ context "when primary key and returning overlap" do
140
+ let(:result) { Book.import(books, returning: %w(id title)) }
141
+
142
+ setup { result }
143
+
144
+ it "returns ids" do
145
+ assert_equal [book_id], result.ids
146
+ end
147
+
148
+ it "returns specified columns" do
149
+ assert_equal [[book_id, 'It']], result.results
51
150
  end
52
151
  end
53
152
 
54
- [{:recursive => false}, {}].each do |import_options|
55
- it "skips recursion for #{import_options.to_s}" do
56
- assert_difference "Book.count", 0 do
57
- Topic.import new_topics, import_options
153
+ context "setting model attributes" do
154
+ let(:code) { 'abc' }
155
+ let(:discount) { 0.10 }
156
+ let(:original_promotion) do
157
+ Promotion.new(code: code, discount: discount)
158
+ end
159
+ let(:updated_promotion) do
160
+ Promotion.new(code: code, description: 'ABC discount')
161
+ end
162
+ let(:returning_columns) { %w(discount) }
163
+
164
+ setup do
165
+ Promotion.import([original_promotion])
166
+ Promotion.import([updated_promotion],
167
+ on_duplicate_key_update: { conflict_target: %i(code), columns: %i(description) },
168
+ returning: returning_columns)
169
+ end
170
+
171
+ it "sets model attributes" do
172
+ assert_equal updated_promotion.discount, discount
173
+ end
174
+
175
+ context "returning multiple columns" do
176
+ let(:returning_columns) { %w(discount description) }
177
+
178
+ it "sets model attributes" do
179
+ assert_equal updated_promotion.discount, discount
58
180
  end
59
181
  end
60
182
  end
183
+ end
184
+ end
185
+
186
+ if ENV['AR_VERSION'].to_f >= 4.0
187
+ describe "with a uuid primary key" do
188
+ let(:vendor) { Vendor.new(name: "foo") }
189
+ let(:vendors) { [vendor] }
190
+
191
+ it "creates records" do
192
+ assert_difference "Vendor.count", +1 do
193
+ Vendor.import vendors
194
+ end
195
+ end
196
+
197
+ it "assigns an id to the model objects" do
198
+ Vendor.import vendors
199
+ assert_not_nil vendor.id
200
+ end
201
+ end
202
+
203
+ describe "with an assigned uuid primary key" do
204
+ let(:id) { SecureRandom.uuid }
205
+ let(:vendor) { Vendor.new(id: id, name: "foo") }
206
+ let(:vendors) { [vendor] }
207
+
208
+ it "creates records with correct id" do
209
+ assert_difference "Vendor.count", +1 do
210
+ Vendor.import vendors
211
+ end
212
+ assert_equal id, vendor.id
213
+ end
214
+ end
215
+ end
216
+
217
+ describe "with store accessor fields" do
218
+ if ENV['AR_VERSION'].to_f >= 4.0
219
+ it "imports values for json fields" do
220
+ vendors = [Vendor.new(name: 'Vendor 1', size: 100)]
221
+ assert_difference "Vendor.count", +1 do
222
+ Vendor.import vendors
223
+ end
224
+ assert_equal(100, Vendor.first.size)
225
+ end
226
+
227
+ it "imports values for hstore fields" do
228
+ vendors = [Vendor.new(name: 'Vendor 1', contact: 'John Smith')]
229
+ assert_difference "Vendor.count", +1 do
230
+ Vendor.import vendors
231
+ end
232
+ assert_equal('John Smith', Vendor.first.contact)
233
+ end
234
+ end
235
+
236
+ if ENV['AR_VERSION'].to_f >= 4.2
237
+ it "imports values for jsonb fields" do
238
+ vendors = [Vendor.new(name: 'Vendor 1', charge_code: '12345')]
239
+ assert_difference "Vendor.count", +1 do
240
+ Vendor.import vendors
241
+ end
242
+ assert_equal('12345', Vendor.first.charge_code)
243
+ end
244
+ end
245
+ end
246
+
247
+ if ENV['AR_VERSION'].to_f >= 4.2
248
+ describe "with serializable fields" do
249
+ it "imports default values as correct data type" do
250
+ vendors = [Vendor.new(name: 'Vendor 1')]
251
+ assert_difference "Vendor.count", +1 do
252
+ Vendor.import vendors
253
+ end
254
+ assert_equal({}, Vendor.first.json_data)
255
+ end
256
+ end
257
+
258
+ %w(json jsonb).each do |json_type|
259
+ describe "with pure #{json_type} fields" do
260
+ let(:data) { { a: :b } }
261
+ let(:json_field_name) { "pure_#{json_type}_data" }
262
+ it "imports the values from saved records" do
263
+ vendor = Vendor.create!(name: 'Vendor 1', json_field_name => data)
264
+
265
+ Vendor.import [vendor], on_duplicate_key_update: [json_field_name]
266
+ assert_equal(data.as_json, vendor.reload[json_field_name])
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ describe "with enum field" do
273
+ let(:vendor_type) { "retailer" }
274
+ it "imports the correct values for enum fields" do
275
+ vendor = Vendor.new(name: 'Vendor 1', vendor_type: vendor_type)
276
+ assert_difference "Vendor.count", +1 do
277
+ Vendor.import [vendor]
278
+ end
279
+ assert_equal(vendor_type, Vendor.first.vendor_type)
280
+ end
281
+ end
282
+
283
+ describe "with binary field" do
284
+ let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".force_encoding('ASCII-8BIT') }
285
+ it "imports the correct values for binary fields" do
286
+ alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
287
+ assert_difference "Alarm.count", +1 do
288
+ Alarm.import alarms
289
+ end
290
+ assert_equal(binary_value, Alarm.first.secret_key)
291
+ end
292
+ end
293
+ end
294
+
295
+ def should_support_postgresql_upsert_functionality
296
+ should_support_basic_on_duplicate_key_update
297
+ should_support_on_duplicate_key_ignore
298
+
299
+ describe "#import" do
300
+ extend ActiveSupport::TestCase::ImportAssertions
301
+
302
+ macro(:perform_import) { raise "supply your own #perform_import in a context below" }
303
+ macro(:updated_topic) { Topic.find(@topic.id) }
304
+
305
+ context "with :on_duplicate_key_ignore and validation checks turned off" do
306
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
307
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
308
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
309
+
310
+ setup do
311
+ Topic.import columns, values, validate: false
312
+ end
313
+
314
+ it "should not update any records" do
315
+ result = Topic.import columns, updated_values, on_duplicate_key_ignore: true, validate: false
316
+ assert_equal [], result.ids
317
+ end
318
+ end
319
+
320
+ context "with :on_duplicate_key_ignore and :recursive enabled" do
321
+ let(:new_topic) { Build(1, :topic_with_book) }
322
+ let(:mixed_topics) { Build(1, :topic_with_book) + new_topic + Build(1, :topic_with_book) }
323
+
324
+ setup do
325
+ Topic.import new_topic, recursive: true
326
+ end
327
+
328
+ # Recursive import depends on the primary keys of the parent model being returned
329
+ # on insert. With on_duplicate_key_ignore enabled, not all ids will be returned
330
+ # and it is possible that a model will be assigned the wrong id and then its children
331
+ # would be associated with the wrong parent.
332
+ it ":on_duplicate_key_ignore is ignored" do
333
+ assert_raise ActiveRecord::RecordNotUnique do
334
+ Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true, validate: false
335
+ end
336
+ end
337
+ end
338
+
339
+ context "with :on_duplicate_key_update and validation checks turned off" do
340
+ asssertion_group(:should_support_on_duplicate_key_update) do
341
+ should_not_update_fields_not_mentioned
342
+ should_update_foreign_keys
343
+ should_not_update_created_at_on_timestamp_columns
344
+ should_update_updated_at_on_timestamp_columns
345
+ end
346
+
347
+ context "using a hash" do
348
+ context "with :columns :all" do
349
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
350
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
351
+
352
+ macro(:perform_import) do |*opts|
353
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
354
+ end
355
+
356
+ setup do
357
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
358
+ Topic.import columns + ['replies_count'], values, validate: false
359
+ end
360
+
361
+ it "should update all specified columns" do
362
+ perform_import
363
+ updated_topic = Topic.find(99)
364
+ assert_equal 'Book - 2nd Edition', updated_topic.title
365
+ assert_equal 'Jane Doe', updated_topic.author_name
366
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
367
+ assert_equal 57, updated_topic.parent_id
368
+ assert_equal 3, updated_topic.replies_count
369
+ end
370
+ end
371
+
372
+ context "with :columns a hash" do
373
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
374
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
375
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
376
+
377
+ macro(:perform_import) do |*opts|
378
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: update_columns }, validate: false)
379
+ end
380
+
381
+ setup do
382
+ Topic.import columns, values, validate: false
383
+ @topic = Topic.find 99
384
+ end
385
+
386
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
387
+ assert_nothing_raised do
388
+ columns = %w(title author_name).freeze
389
+ Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
390
+ end
391
+ end
392
+
393
+ context "using string hash map" do
394
+ let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
395
+ should_support_on_duplicate_key_update
396
+ should_update_fields_mentioned
397
+ end
398
+
399
+ context "using string hash map, but specifying column mismatches" do
400
+ let(:update_columns) { { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
401
+ should_support_on_duplicate_key_update
402
+ should_update_fields_mentioned_with_hash_mappings
403
+ end
404
+
405
+ context "using symbol hash map" do
406
+ let(:update_columns) { { title: :title, author_email_address: :author_email_address, parent_id: :parent_id } }
407
+ should_support_on_duplicate_key_update
408
+ should_update_fields_mentioned
409
+ end
410
+
411
+ context "using symbol hash map, but specifying column mismatches" do
412
+ let(:update_columns) { { title: :author_email_address, author_email_address: :title, parent_id: :parent_id } }
413
+ should_support_on_duplicate_key_update
414
+ should_update_fields_mentioned_with_hash_mappings
415
+ end
416
+ end
61
417
 
62
- it 'imports deeper nested associations' do
63
- assert_difference "Chapter.count", +num_chapters do
64
- assert_difference "EndNote.count", +num_endnotes do
65
- Topic.import new_topics, :recursive => true
66
- new_topics.each do |topic|
67
- topic.books.each do |book|
68
- book.chapters.each do |chapter|
69
- assert_equal book.id, chapter.book_id
70
- end
71
- book.end_notes.each do |endnote|
72
- assert_equal book.id, endnote.book_id
73
- end
418
+ context 'with :index_predicate' do
419
+ let(:columns) { %w( id device_id alarm_type status metadata ) }
420
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
421
+ let(:updated_values) { [[99, 17, 1, 2, 'bar']] }
422
+
423
+ macro(:perform_import) do |*opts|
424
+ 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)
425
+ end
426
+
427
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
428
+
429
+ setup do
430
+ Alarm.import columns, values, validate: false
431
+ @alarm = Alarm.find 99
432
+ end
433
+
434
+ context 'supports on duplicate key update for partial indexes' do
435
+ it 'should not update created_at timestamp columns' do
436
+ Timecop.freeze Chronic.parse("5 minutes from now") do
437
+ perform_import
438
+ assert_in_delta @alarm.created_at.to_i, updated_alarm.created_at.to_i, 1
74
439
  end
75
440
  end
441
+
442
+ it 'should update updated_at timestamp columns' do
443
+ time = Chronic.parse("5 minutes from now")
444
+ Timecop.freeze time do
445
+ perform_import
446
+ assert_in_delta time.to_i, updated_alarm.updated_at.to_i, 1
447
+ end
448
+ end
449
+
450
+ it 'should not update fields not mentioned' do
451
+ perform_import
452
+ assert_equal 'foo', updated_alarm.metadata
453
+ end
454
+
455
+ it 'should update fields mentioned with hash mappings' do
456
+ perform_import
457
+ assert_equal 2, updated_alarm.status
458
+ end
76
459
  end
77
460
  end
78
- end
79
461
 
80
- it "skips validation of the associations if requested" do
81
- assert_difference "Chapter.count", +num_chapters do
82
- Topic.import new_topics_with_invalid_chapter, :validate => false, :recursive => true
462
+ context 'with :condition' do
463
+ let(:columns) { %w( id device_id alarm_type status metadata) }
464
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
465
+ let(:updated_values) { [[99, 17, 1, 1, 'bar']] }
466
+
467
+ macro(:perform_import) do |*opts|
468
+ Alarm.import(
469
+ columns,
470
+ updated_values,
471
+ opts.extract_options!.merge(
472
+ on_duplicate_key_update: {
473
+ conflict_target: [:id],
474
+ condition: "alarms.metadata NOT LIKE '%foo%'",
475
+ columns: [:metadata]
476
+ },
477
+ validate: false
478
+ )
479
+ )
480
+ end
481
+
482
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
483
+
484
+ setup do
485
+ Alarm.import columns, values, validate: false
486
+ @alarm = Alarm.find 99
487
+ end
488
+
489
+ it 'should not update fields not matched' do
490
+ perform_import
491
+ assert_equal 'foo', updated_alarm.metadata
492
+ end
83
493
  end
84
- end
85
494
 
86
- # These models dont validate associated. So we expect that books and topics get inserted, but not chapters
87
- # Putting a transaction around everything wouldn't work, so if you want your chapters to prevent topics from
88
- # being created, you would need to have validates_associated in your models and insert with validation
89
- describe "all_or_none" do
90
- [Book, Topic, EndNote].each do |type|
91
- it "creates #{type.to_s}" do
92
- assert_difference "#{type.to_s}.count", send("num_#{type.to_s.downcase}s") do
93
- Topic.import new_topics_with_invalid_chapter, :all_or_none => true, :recursive => true
495
+ context "with :constraint_name" do
496
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
497
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
498
+ let(:updated_values) { [[100, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
499
+
500
+ macro(:perform_import) do |*opts|
501
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { constraint_name: :topics_pkey, columns: update_columns }, validate: false)
502
+ end
503
+
504
+ setup do
505
+ Topic.import columns, values, validate: false
506
+ @topic = Topic.find 100
507
+ end
508
+
509
+ let(:update_columns) { [:title, :author_email_address, :parent_id] }
510
+ should_support_on_duplicate_key_update
511
+ should_update_fields_mentioned
512
+ end
513
+
514
+ context "default to the primary key" do
515
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
516
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
517
+ let(:updated_values) { [[100, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
518
+ let(:update_columns) { [:title, :author_email_address, :parent_id] }
519
+
520
+ setup do
521
+ Topic.import columns, values, validate: false
522
+ @topic = Topic.find 100
523
+ end
524
+
525
+ context "with no :conflict_target or :constraint_name" do
526
+ macro(:perform_import) do |*opts|
527
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { columns: update_columns }, validate: false)
528
+ end
529
+
530
+ should_support_on_duplicate_key_update
531
+ should_update_fields_mentioned
532
+ end
533
+
534
+ context "with empty value for :conflict_target" do
535
+ macro(:perform_import) do |*opts|
536
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: [], columns: update_columns }, validate: false)
537
+ end
538
+
539
+ should_support_on_duplicate_key_update
540
+ should_update_fields_mentioned
541
+ end
542
+
543
+ context "with empty value for :constraint_name" do
544
+ macro(:perform_import) do |*opts|
545
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { constraint_name: '', columns: update_columns }, validate: false)
546
+ end
547
+
548
+ should_support_on_duplicate_key_update
549
+ should_update_fields_mentioned
550
+ end
551
+ end
552
+
553
+ context "with no :conflict_target or :constraint_name" do
554
+ context "with no primary key" do
555
+ it "raises ArgumentError" do
556
+ error = assert_raises ArgumentError do
557
+ Rule.import Build(3, :rules), on_duplicate_key_update: [:condition_text], validate: false
558
+ end
559
+ assert_match(/Expected :conflict_target or :constraint_name to be specified/, error.message)
94
560
  end
95
561
  end
96
562
  end
97
- it "doesn't create chapters" do
98
- assert_difference "Chapter.count", 0 do
99
- Topic.import new_topics_with_invalid_chapter, :all_or_none => true, :recursive => true
563
+
564
+ context "with no :columns" do
565
+ let(:columns) { %w( id title author_name author_email_address ) }
566
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com"]] }
567
+ let(:updated_values) { [[100, "Title Should Not Change", "Author Should Not Change", "john@nogo.com"]] }
568
+
569
+ macro(:perform_import) do |*opts|
570
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id }, validate: false)
100
571
  end
572
+
573
+ setup do
574
+ Topic.import columns, values, validate: false
575
+ @topic = Topic.find 100
576
+ end
577
+
578
+ should_update_updated_at_on_timestamp_columns
101
579
  end
102
580
  end
103
581
  end
@@ -0,0 +1,43 @@
1
+ def should_support_on_duplicate_key_ignore
2
+ describe "#import" do
3
+ extend ActiveSupport::TestCase::ImportAssertions
4
+ let(:topic) { Topic.create!(title: "Book", author_name: "John Doe") }
5
+ let(:topics) { [topic] }
6
+
7
+ context "with :on_duplicate_key_ignore" do
8
+ it "should skip duplicates and continue import" do
9
+ topics << Topic.new(title: "Book 2", author_name: "Jane Doe")
10
+ assert_difference "Topic.count", +1 do
11
+ result = Topic.import topics, on_duplicate_key_ignore: true, validate: false
12
+ assert_not_equal topics.first.id, result.ids.first
13
+ assert_nil topics.last.id
14
+ end
15
+ end
16
+
17
+ unless ENV["SKIP_COMPOSITE_PK"]
18
+ context "with composite primary keys" do
19
+ it "should import array of values successfully" do
20
+ columns = [:tag_id, :publisher_id, :tag]
21
+ values = [[1, 1, 'Mystery'], [1, 1, 'Science']]
22
+
23
+ assert_difference "Tag.count", +1 do
24
+ Tag.import columns, values, on_duplicate_key_ignore: true, validate: false
25
+ end
26
+ assert_equal 'Mystery', Tag.first.tag
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ context "with :ignore" do
33
+ it "should skip duplicates and continue import" do
34
+ topics << Topic.new(title: "Book 2", author_name: "Jane Doe")
35
+ assert_difference "Topic.count", +1 do
36
+ result = Topic.import topics, ignore: true, validate: false
37
+ assert_not_equal topics.first.id, result.ids.first
38
+ assert_nil topics.last.id
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end