activerecord-import 0.10.0 → 1.0.8

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 (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