activerecord-import 0.19.0 → 0.28.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +22 -12
  3. data/CHANGELOG.md +151 -0
  4. data/Gemfile +13 -10
  5. data/README.markdown +548 -5
  6. data/Rakefile +2 -1
  7. data/benchmarks/lib/cli_parser.rb +2 -1
  8. data/gemfiles/5.1.gemfile +1 -0
  9. data/gemfiles/5.2.gemfile +2 -0
  10. data/lib/activerecord-import/adapters/abstract_adapter.rb +2 -2
  11. data/lib/activerecord-import/adapters/mysql_adapter.rb +15 -6
  12. data/lib/activerecord-import/adapters/postgresql_adapter.rb +59 -15
  13. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +126 -3
  14. data/lib/activerecord-import/base.rb +4 -6
  15. data/lib/activerecord-import/import.rb +381 -126
  16. data/lib/activerecord-import/synchronize.rb +1 -1
  17. data/lib/activerecord-import/value_sets_parser.rb +14 -0
  18. data/lib/activerecord-import/version.rb +1 -1
  19. data/lib/activerecord-import.rb +2 -15
  20. data/test/adapters/makara_postgis.rb +1 -0
  21. data/test/import_test.rb +148 -14
  22. data/test/makara_postgis/import_test.rb +8 -0
  23. data/test/models/account.rb +3 -0
  24. data/test/models/bike_maker.rb +7 -0
  25. data/test/models/topic.rb +10 -0
  26. data/test/models/user.rb +3 -0
  27. data/test/models/user_token.rb +4 -0
  28. data/test/schema/generic_schema.rb +20 -0
  29. data/test/schema/mysql2_schema.rb +19 -0
  30. data/test/schema/postgresql_schema.rb +1 -0
  31. data/test/schema/sqlite3_schema.rb +13 -0
  32. data/test/support/factories.rb +9 -8
  33. data/test/support/generate.rb +6 -6
  34. data/test/support/mysql/import_examples.rb +14 -2
  35. data/test/support/postgresql/import_examples.rb +136 -0
  36. data/test/support/shared_examples/on_duplicate_key_update.rb +252 -1
  37. data/test/support/shared_examples/recursive_import.rb +41 -11
  38. data/test/support/sqlite3/import_examples.rb +187 -10
  39. data/test/synchronize_test.rb +8 -0
  40. data/test/test_helper.rb +9 -1
  41. data/test/value_sets_bytes_parser_test.rb +13 -2
  42. metadata +19 -4
  43. data/test/schema/mysql_schema.rb +0 -16
@@ -24,6 +24,30 @@ def should_support_postgresql_import_functionality
24
24
  end
25
25
  end
26
26
 
27
+ context "setting attributes and marking clean" do
28
+ let(:topic) { Build(:topics) }
29
+
30
+ setup { Topic.import([topic]) }
31
+
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
+ it "marks models as persisted" do
41
+ assert !topic.new_record?
42
+ assert topic.persisted?
43
+ end
44
+
45
+ it "assigns timestamps" do
46
+ assert topic.created_at.present?
47
+ assert topic.updated_at.present?
48
+ end
49
+ end
50
+
27
51
  describe "with query cache enabled" do
28
52
  setup do
29
53
  unless ActiveRecord::Base.connection.query_cache_enabled
@@ -61,6 +85,76 @@ def should_support_postgresql_import_functionality
61
85
  assert_equal [], Book.import(books, no_returning: true).ids
62
86
  end
63
87
  end
88
+
89
+ describe "returning" do
90
+ let(:books) { [Book.new(author_name: "King", title: "It")] }
91
+ let(:result) { Book.import(books, returning: %w(author_name title)) }
92
+ let(:book_id) do
93
+ if RUBY_PLATFORM == 'java' || ENV['AR_VERSION'].to_i >= 5.0
94
+ books.first.id
95
+ else
96
+ books.first.id.to_s
97
+ end
98
+ end
99
+
100
+ it "creates records" do
101
+ assert_difference("Book.count", +1) { result }
102
+ end
103
+
104
+ it "returns ids" do
105
+ result
106
+ assert_equal [book_id], result.ids
107
+ end
108
+
109
+ it "returns specified columns" do
110
+ assert_equal [%w(King It)], result.results
111
+ end
112
+
113
+ context "when primary key and returning overlap" do
114
+ let(:result) { Book.import(books, returning: %w(id title)) }
115
+
116
+ setup { result }
117
+
118
+ it "returns ids" do
119
+ assert_equal [book_id], result.ids
120
+ end
121
+
122
+ it "returns specified columns" do
123
+ assert_equal [[book_id, 'It']], result.results
124
+ end
125
+ end
126
+
127
+ context "setting model attributes" do
128
+ let(:code) { 'abc' }
129
+ let(:discount) { 0.10 }
130
+ let(:original_promotion) do
131
+ Promotion.new(code: code, discount: discount)
132
+ end
133
+ let(:updated_promotion) do
134
+ Promotion.new(code: code, description: 'ABC discount')
135
+ end
136
+ let(:returning_columns) { %w(discount) }
137
+
138
+ setup do
139
+ Promotion.import([original_promotion])
140
+ Promotion.import([updated_promotion],
141
+ on_duplicate_key_update: { conflict_target: %i(code), columns: %i(description) },
142
+ returning: returning_columns)
143
+ end
144
+
145
+ it "sets model attributes" do
146
+ assert_equal updated_promotion.discount, discount
147
+ end
148
+
149
+ context "returning multiple columns" do
150
+ let(:returning_columns) { %w(discount description) }
151
+
152
+ it "sets model attributes" do
153
+ assert_equal updated_promotion.discount, discount
154
+ end
155
+ end
156
+ end
157
+ end
64
158
  end
65
159
 
66
160
  if ENV['AR_VERSION'].to_f >= 4.0
@@ -135,6 +229,17 @@ def should_support_postgresql_import_functionality
135
229
  end
136
230
  end
137
231
  end
232
+
233
+ describe "with binary field" do
234
+ let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".force_encoding('ASCII-8BIT') }
235
+ it "imports the correct values for binary fields" do
236
+ alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
237
+ assert_difference "Alarm.count", +1 do
238
+ Alarm.import alarms
239
+ end
240
+ assert_equal(binary_value, Alarm.first.secret_key)
241
+ end
242
+ end
138
243
  end
139
244
 
140
245
  def should_support_postgresql_upsert_functionality
@@ -190,6 +295,30 @@ def should_support_postgresql_upsert_functionality
190
295
  end
191
296
 
192
297
  context "using a hash" do
298
+ context "with :columns :all" do
299
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
300
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
301
+
302
+ macro(:perform_import) do |*opts|
303
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
304
+ end
305
+
306
+ setup do
307
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
308
+ Topic.import columns + ['replies_count'], values, validate: false
309
+ end
310
+
311
+ it "should update all specified columns" do
312
+ perform_import
313
+ updated_topic = Topic.find(99)
314
+ assert_equal 'Book - 2nd Edition', updated_topic.title
315
+ assert_equal 'Jane Doe', updated_topic.author_name
316
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
317
+ assert_equal 57, updated_topic.parent_id
318
+ assert_equal 3, updated_topic.replies_count
319
+ end
320
+ end
321
+
193
322
  context "with :columns a hash" do
194
323
  let(:columns) { %w( id title author_name author_email_address parent_id ) }
195
324
  let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
@@ -204,6 +333,13 @@ def should_support_postgresql_upsert_functionality
204
333
  @topic = Topic.find 99
205
334
  end
206
335
 
336
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
337
+ assert_nothing_raised do
338
+ columns = %w(title author_name).freeze
339
+ Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
340
+ end
341
+ end
342
+
207
343
  context "using string hash map" do
208
344
  let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
209
345
  should_support_on_duplicate_key_update
@@ -5,9 +5,240 @@ def should_support_basic_on_duplicate_key_update
5
5
  macro(:perform_import) { raise "supply your own #perform_import in a context below" }
6
6
  macro(:updated_topic) { Topic.find(@topic.id) }
7
7
 
8
+ context "with lock_version upsert" do
9
+ describe 'optimistic lock' do
10
+ it 'lock_version upsert after on_duplcate_key_update by model' do
11
+ users = [
12
+ User.new(name: 'Salomon'),
13
+ User.new(name: 'Nathan')
14
+ ]
15
+ User.import(users)
16
+ assert User.count == users.length
17
+ User.all.each do |user|
18
+ assert_equal 0, user.lock_version
19
+ end
20
+ updated_users = User.all.map do |user|
21
+ user.name += ' Rothschild'
22
+ user
23
+ end
24
+ User.import(updated_users, on_duplicate_key_update: [:name])
25
+ assert User.count == updated_users.length
26
+ User.all.each_with_index do |user, i|
27
+ assert_equal user.name, users[i].name + ' Rothschild'
28
+ assert_equal 1, user.lock_version
29
+ end
30
+ end
31
+
32
+ it 'lock_version upsert after on_duplcate_key_update by array' do
33
+ users = [
34
+ User.new(name: 'Salomon'),
35
+ User.new(name: 'Nathan')
36
+ ]
37
+ User.import(users)
38
+ assert User.count == users.length
39
+ User.all.each do |user|
40
+ assert_equal 0, user.lock_version
41
+ end
42
+
43
+ columns = [:id, :name]
44
+ updated_values = User.all.map do |user|
45
+ user.name += ' Rothschild'
46
+ [user.id, user.name]
47
+ end
48
+ User.import(columns, updated_values, on_duplicate_key_update: [:name])
49
+ assert User.count == updated_values.length
50
+ User.all.each_with_index do |user, i|
51
+ assert_equal user.name, users[i].name + ' Rothschild'
52
+ assert_equal 1, user.lock_version
53
+ end
54
+ end
55
+
56
+ it 'lock_version upsert after on_duplcate_key_update by hash' do
57
+ users = [
58
+ User.new(name: 'Salomon'),
59
+ User.new(name: 'Nathan')
60
+ ]
61
+ User.import(users)
62
+ assert User.count == users.length
63
+ User.all.each do |user|
64
+ assert_equal 0, user.lock_version
65
+ end
66
+ updated_values = User.all.map do |user|
67
+ user.name += ' Rothschild'
68
+ { id: user.id, name: user.name }
69
+ end
70
+ User.import(updated_values, on_duplicate_key_update: [:name])
71
+ assert User.count == updated_values.length
72
+ User.all.each_with_index do |user, i|
73
+ assert_equal user.name, users[i].name + ' Rothschild'
74
+ assert_equal 1, user.lock_version
75
+ end
76
+ end
77
+
78
+ it 'upsert optimistic lock columns other than lock_version by model' do
79
+ accounts = [
80
+ Account.new(name: 'Salomon'),
81
+ Account.new(name: 'Nathan')
82
+ ]
83
+ Account.import(accounts)
84
+ assert Account.count == accounts.length
85
+ Account.all.each do |user|
86
+ assert_equal 0, user.lock
87
+ end
88
+ updated_accounts = Account.all.map do |user|
89
+ user.name += ' Rothschild'
90
+ user
91
+ end
92
+ Account.import(updated_accounts, on_duplicate_key_update: [:id, :name])
93
+ assert Account.count == updated_accounts.length
94
+ Account.all.each_with_index do |user, i|
95
+ assert_equal user.name, accounts[i].name + ' Rothschild'
96
+ assert_equal 1, user.lock
97
+ end
98
+ end
99
+
100
+ it 'upsert optimistic lock columns other than lock_version by array' do
101
+ accounts = [
102
+ Account.new(name: 'Salomon'),
103
+ Account.new(name: 'Nathan')
104
+ ]
105
+ Account.import(accounts)
106
+ assert Account.count == accounts.length
107
+ Account.all.each do |user|
108
+ assert_equal 0, user.lock
109
+ end
110
+
111
+ columns = [:id, :name]
112
+ updated_values = Account.all.map do |user|
113
+ user.name += ' Rothschild'
114
+ [user.id, user.name]
115
+ end
116
+ Account.import(columns, updated_values, on_duplicate_key_update: [:name])
117
+ assert Account.count == updated_values.length
118
+ Account.all.each_with_index do |user, i|
119
+ assert_equal user.name, accounts[i].name + ' Rothschild'
120
+ assert_equal 1, user.lock
121
+ end
122
+ end
123
+
124
+ it 'upsert optimistic lock columns other than lock_version by hash' do
125
+ accounts = [
126
+ Account.new(name: 'Salomon'),
127
+ Account.new(name: 'Nathan')
128
+ ]
129
+ Account.import(accounts)
130
+ assert Account.count == accounts.length
131
+ Account.all.each do |user|
132
+ assert_equal 0, user.lock
133
+ end
134
+ updated_values = Account.all.map do |user|
135
+ user.name += ' Rothschild'
136
+ { id: user.id, name: user.name }
137
+ end
138
+ Account.import(updated_values, on_duplicate_key_update: [:name])
139
+ assert Account.count == updated_values.length
140
+ Account.all.each_with_index do |user, i|
141
+ assert_equal user.name, accounts[i].name + ' Rothschild'
142
+ assert_equal 1, user.lock
143
+ end
144
+ end
145
+
146
+ it 'update the lock_version of models separated by namespaces by model' do
147
+ makers = [
148
+ Bike::Maker.new(name: 'Yamaha'),
149
+ Bike::Maker.new(name: 'Honda')
150
+ ]
151
+ Bike::Maker.import(makers)
152
+ assert Bike::Maker.count == makers.length
153
+ Bike::Maker.all.each do |maker|
154
+ assert_equal 0, maker.lock_version
155
+ end
156
+ updated_makers = Bike::Maker.all.map do |maker|
157
+ maker.name += ' bikes'
158
+ maker
159
+ end
160
+ Bike::Maker.import(updated_makers, on_duplicate_key_update: [:name])
161
+ assert Bike::Maker.count == updated_makers.length
162
+ Bike::Maker.all.each_with_index do |maker, i|
163
+ assert_equal maker.name, makers[i].name + ' bikes'
164
+ assert_equal 1, maker.lock_version
165
+ end
166
+ end
167
+ it 'update the lock_version of models separated by namespaces by array' do
168
+ makers = [
169
+ Bike::Maker.new(name: 'Yamaha'),
170
+ Bike::Maker.new(name: 'Honda')
171
+ ]
172
+ Bike::Maker.import(makers)
173
+ assert Bike::Maker.count == makers.length
174
+ Bike::Maker.all.each do |maker|
175
+ assert_equal 0, maker.lock_version
176
+ end
177
+
178
+ columns = [:id, :name]
179
+ updated_values = Bike::Maker.all.map do |maker|
180
+ maker.name += ' bikes'
181
+ [maker.id, maker.name]
182
+ end
183
+ Bike::Maker.import(columns, updated_values, on_duplicate_key_update: [:name])
184
+ assert Bike::Maker.count == updated_values.length
185
+ Bike::Maker.all.each_with_index do |maker, i|
186
+ assert_equal maker.name, makers[i].name + ' bikes'
187
+ assert_equal 1, maker.lock_version
188
+ end
189
+ end
190
+
191
+ it 'update the lock_version of models separated by namespaces by hash' do
192
+ makers = [
193
+ Bike::Maker.new(name: 'Yamaha'),
194
+ Bike::Maker.new(name: 'Honda')
195
+ ]
196
+ Bike::Maker.import(makers)
197
+ assert Bike::Maker.count == makers.length
198
+ Bike::Maker.all.each do |maker|
199
+ assert_equal 0, maker.lock_version
200
+ end
201
+ updated_values = Bike::Maker.all.map do |maker|
202
+ maker.name += ' bikes'
203
+ { id: maker.id, name: maker.name }
204
+ end
205
+ Bike::Maker.import(updated_values, on_duplicate_key_update: [:name])
206
+ assert Bike::Maker.count == updated_values.length
207
+ Bike::Maker.all.each_with_index do |maker, i|
208
+ assert_equal maker.name, makers[i].name + ' bikes'
209
+ assert_equal 1, maker.lock_version
210
+ end
211
+ end
212
+ end
213
+ end
214
+
8
215
  context "with :on_duplicate_key_update" do
216
+ describe 'using :all' do
217
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
218
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
219
+
220
+ macro(:perform_import) do |*opts|
221
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: :all, validate: false)
222
+ end
223
+
224
+ setup do
225
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
226
+ Topic.import columns + ['replies_count'], values, validate: false
227
+ end
228
+
229
+ it 'updates all specified columns' do
230
+ perform_import
231
+ updated_topic = Topic.find(99)
232
+ assert_equal 'Book - 2nd Edition', updated_topic.title
233
+ assert_equal 'Jane Doe', updated_topic.author_name
234
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
235
+ assert_equal 57, updated_topic.parent_id
236
+ assert_equal 3, updated_topic.replies_count
237
+ end
238
+ end
239
+
9
240
  describe "argument safety" do
10
- it "should not modify the passed in :on_duplicate_key_update columns array" do
241
+ it "should not modify the passed in :on_duplicate_key_update array" do
11
242
  assert_nothing_raised do
12
243
  columns = %w(title author_name).freeze
13
244
  Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: columns
@@ -15,6 +246,26 @@ def should_support_basic_on_duplicate_key_update
15
246
  end
16
247
  end
17
248
 
249
+ context "with timestamps enabled" do
250
+ let(:time) { Chronic.parse("5 minutes from now") }
251
+
252
+ it 'should not overwrite changed updated_at with current timestamp' do
253
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
254
+ timestamp = Time.now.utc
255
+ topic.updated_at = timestamp
256
+ Topic.import [topic], on_duplicate_key_update: :all, validate: false
257
+ assert_equal timestamp.to_s, Topic.last.updated_at.to_s
258
+ end
259
+
260
+ it 'should update updated_at with current timestamp' do
261
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
262
+ Timecop.freeze(time) do
263
+ Topic.import [topic], on_duplicate_key_update: [:updated_at], validate: false
264
+ assert_in_delta time.to_i, topic.reload.updated_at.to_i, 1.second
265
+ end
266
+ end
267
+ end
268
+
18
269
  context "with validation checks turned off" do
19
270
  asssertion_group(:should_support_on_duplicate_key_update) do
20
271
  should_not_update_fields_not_mentioned
@@ -11,7 +11,7 @@ def should_support_recursive_import
11
11
  let(:num_chapters) { 18 }
12
12
  let(:num_endnotes) { 24 }
13
13
 
14
- let(:new_question_with_rule) { FactoryGirl.build :question, :with_rule }
14
+ let(:new_question_with_rule) { FactoryBot.build :question, :with_rule }
15
15
 
16
16
  it 'imports top level' do
17
17
  assert_difference "Topic.count", +num_topics do
@@ -90,6 +90,19 @@ def should_support_recursive_import
90
90
  end
91
91
  end
92
92
 
93
+ # Models are only valid if all associations are valid
94
+ it "only imports models with valid associations" do
95
+ assert_difference "Topic.count", 2 do
96
+ assert_difference "Book.count", 4 do
97
+ assert_difference "Chapter.count", 12 do
98
+ assert_difference "EndNote.count", 16 do
99
+ Topic.import new_topics_with_invalid_chapter, recursive: true
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
93
106
  it "skips validation of the associations if requested" do
94
107
  assert_difference "Chapter.count", +num_chapters do
95
108
  Topic.import new_topics_with_invalid_chapter, validate: false, recursive: true
@@ -102,6 +115,31 @@ def should_support_recursive_import
102
115
  end
103
116
  end
104
117
 
118
+ it "imports an imported belongs_to association id" do
119
+ first_new_topic = new_topics[0]
120
+ second_new_topic = new_topics[1]
121
+
122
+ books = first_new_topic.books.to_a
123
+ Topic.import new_topics, validate: false
124
+
125
+ assert_difference "Book.count", books.size do
126
+ Book.import books, validate: false
127
+ end
128
+
129
+ books.each do |book|
130
+ assert_equal book.topic_id, first_new_topic.id
131
+ end
132
+
133
+ books.each { |book| book.topic_id = second_new_topic.id }
134
+ assert_no_difference "Book.count", books.size do
135
+ Book.import books, validate: false, on_duplicate_key_update: [:topic_id]
136
+ end
137
+
138
+ books.each do |book|
139
+ assert_equal book.topic_id, second_new_topic.id
140
+ end
141
+ end
142
+
105
143
  unless ENV["SKIP_COMPOSITE_PK"]
106
144
  describe "with composite primary keys" do
107
145
  it "should import models and set id" do
@@ -119,22 +157,14 @@ def should_support_recursive_import
119
157
  end
120
158
  end
121
159
 
122
- # These models dont validate associated. So we expect that books and topics get inserted, but not chapters
123
- # Putting a transaction around everything wouldn't work, so if you want your chapters to prevent topics from
124
- # being created, you would need to have validates_associated in your models and insert with validation
125
160
  describe "all_or_none" do
126
- [Book, Topic, EndNote].each do |type|
161
+ [Book, Chapter, Topic, EndNote].each do |type|
127
162
  it "creates #{type}" do
128
- assert_difference "#{type}.count", send("num_#{type.to_s.downcase}s") do
163
+ assert_difference "#{type}.count", 0 do
129
164
  Topic.import new_topics_with_invalid_chapter, all_or_none: true, recursive: true
130
165
  end
131
166
  end
132
167
  end
133
- it "doesn't create chapters" do
134
- assert_difference "Chapter.count", 0 do
135
- Topic.import new_topics_with_invalid_chapter, all_or_none: true, recursive: true
136
- end
137
- end
138
168
  end
139
169
 
140
170
  # If adapter supports on_duplicate_key_update, it is only applied to top level models so that SQL with invalid