activerecord-import 0.22.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +235 -3
  7. data/Gemfile +22 -15
  8. data/LICENSE +21 -56
  9. data/README.markdown +574 -22
  10. data/Rakefile +4 -1
  11. data/activerecord-import.gemspec +6 -5
  12. data/benchmarks/benchmark.rb +7 -1
  13. data/benchmarks/lib/base.rb +2 -0
  14. data/benchmarks/lib/cli_parser.rb +3 -1
  15. data/benchmarks/lib/float.rb +2 -0
  16. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  17. data/benchmarks/lib/output_to_csv.rb +2 -0
  18. data/benchmarks/lib/output_to_html.rb +4 -2
  19. data/benchmarks/models/test_innodb.rb +2 -0
  20. data/benchmarks/models/test_memory.rb +2 -0
  21. data/benchmarks/models/test_myisam.rb +2 -0
  22. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  23. data/gemfiles/4.2.gemfile +2 -0
  24. data/gemfiles/5.0.gemfile +2 -0
  25. data/gemfiles/5.1.gemfile +2 -0
  26. data/gemfiles/5.2.gemfile +4 -0
  27. data/gemfiles/6.0.gemfile +4 -0
  28. data/gemfiles/6.1.gemfile +4 -0
  29. data/gemfiles/7.0.gemfile +4 -0
  30. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  32. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  33. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  38. data/lib/activerecord-import/adapters/abstract_adapter.rb +10 -2
  39. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  40. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql_adapter.rb +19 -11
  42. data/lib/activerecord-import/adapters/postgresql_adapter.rb +56 -37
  43. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
  44. data/lib/activerecord-import/base.rb +12 -2
  45. data/lib/activerecord-import/import.rb +300 -136
  46. data/lib/activerecord-import/mysql2.rb +2 -0
  47. data/lib/activerecord-import/postgresql.rb +2 -0
  48. data/lib/activerecord-import/sqlite3.rb +2 -0
  49. data/lib/activerecord-import/synchronize.rb +4 -2
  50. data/lib/activerecord-import/value_sets_parser.rb +4 -0
  51. data/lib/activerecord-import/version.rb +3 -1
  52. data/lib/activerecord-import.rb +4 -1
  53. data/test/adapters/jdbcmysql.rb +2 -0
  54. data/test/adapters/jdbcpostgresql.rb +2 -0
  55. data/test/adapters/jdbcsqlite3.rb +2 -0
  56. data/test/adapters/makara_postgis.rb +3 -0
  57. data/test/adapters/mysql2.rb +2 -0
  58. data/test/adapters/mysql2_makara.rb +2 -0
  59. data/test/adapters/mysql2spatial.rb +2 -0
  60. data/test/adapters/postgis.rb +2 -0
  61. data/test/adapters/postgresql.rb +2 -0
  62. data/test/adapters/postgresql_makara.rb +2 -0
  63. data/test/adapters/seamless_database_pool.rb +2 -0
  64. data/test/adapters/spatialite.rb +2 -0
  65. data/test/adapters/sqlite3.rb +2 -0
  66. data/test/{travis → github}/database.yml +3 -1
  67. data/test/import_test.rb +159 -8
  68. data/test/jdbcmysql/import_test.rb +2 -0
  69. data/test/jdbcpostgresql/import_test.rb +2 -0
  70. data/test/jdbcsqlite3/import_test.rb +2 -0
  71. data/test/makara_postgis/import_test.rb +10 -0
  72. data/test/models/account.rb +5 -0
  73. data/test/models/alarm.rb +2 -0
  74. data/test/models/animal.rb +8 -0
  75. data/test/models/bike_maker.rb +9 -0
  76. data/test/models/book.rb +2 -0
  77. data/test/models/car.rb +2 -0
  78. data/test/models/card.rb +5 -0
  79. data/test/models/chapter.rb +2 -0
  80. data/test/models/customer.rb +8 -0
  81. data/test/models/deck.rb +8 -0
  82. data/test/models/dictionary.rb +2 -0
  83. data/test/models/discount.rb +2 -0
  84. data/test/models/end_note.rb +2 -0
  85. data/test/models/group.rb +2 -0
  86. data/test/models/order.rb +8 -0
  87. data/test/models/playing_card.rb +4 -0
  88. data/test/models/promotion.rb +2 -0
  89. data/test/models/question.rb +2 -0
  90. data/test/models/rule.rb +2 -0
  91. data/test/models/tag.rb +3 -0
  92. data/test/models/tag_alias.rb +5 -0
  93. data/test/models/topic.rb +2 -0
  94. data/test/models/user.rb +5 -0
  95. data/test/models/user_token.rb +6 -0
  96. data/test/models/vendor.rb +2 -0
  97. data/test/models/widget.rb +2 -0
  98. data/test/mysql2/import_test.rb +2 -0
  99. data/test/mysql2_makara/import_test.rb +2 -0
  100. data/test/mysqlspatial2/import_test.rb +2 -0
  101. data/test/postgis/import_test.rb +2 -0
  102. data/test/postgresql/import_test.rb +2 -0
  103. data/test/schema/generic_schema.rb +53 -0
  104. data/test/schema/jdbcpostgresql_schema.rb +2 -0
  105. data/test/schema/mysql2_schema.rb +21 -0
  106. data/test/schema/postgis_schema.rb +2 -0
  107. data/test/schema/postgresql_schema.rb +18 -0
  108. data/test/schema/sqlite3_schema.rb +15 -0
  109. data/test/schema/version.rb +2 -0
  110. data/test/sqlite3/import_test.rb +2 -0
  111. data/test/support/active_support/test_case_extensions.rb +2 -0
  112. data/test/support/assertions.rb +2 -0
  113. data/test/support/factories.rb +10 -8
  114. data/test/support/generate.rb +10 -8
  115. data/test/support/mysql/import_examples.rb +14 -1
  116. data/test/support/postgresql/import_examples.rb +140 -3
  117. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  118. data/test/support/shared_examples/on_duplicate_key_update.rb +263 -0
  119. data/test/support/shared_examples/recursive_import.rb +76 -4
  120. data/test/support/sqlite3/import_examples.rb +191 -26
  121. data/test/synchronize_test.rb +2 -0
  122. data/test/test_helper.rb +36 -3
  123. data/test/value_sets_bytes_parser_test.rb +2 -0
  124. data/test/value_sets_records_parser_test.rb +2 -0
  125. metadata +46 -18
  126. data/.travis.yml +0 -61
  127. data/gemfiles/3.2.gemfile +0 -2
  128. data/gemfiles/4.0.gemfile +0 -2
  129. data/gemfiles/4.1.gemfile +0 -2
  130. data/test/schema/mysql_schema.rb +0 -16
@@ -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
@@ -5,7 +7,248 @@ def should_support_basic_on_duplicate_key_update
5
7
  macro(:perform_import) { raise "supply your own #perform_import in a context below" }
6
8
  macro(:updated_topic) { Topic.find(@topic.id) }
7
9
 
10
+ context "with lock_version upsert" do
11
+ describe 'optimistic lock' do
12
+ it 'lock_version upsert after on_duplcate_key_update by model' do
13
+ users = [
14
+ User.new(name: 'Salomon'),
15
+ User.new(name: 'Nathan')
16
+ ]
17
+ User.import(users)
18
+ assert User.count == users.length
19
+ User.all.each do |user|
20
+ assert_equal 0, user.lock_version
21
+ end
22
+ updated_users = User.all.map do |user|
23
+ user.name += ' Rothschild'
24
+ user
25
+ end
26
+ User.import(updated_users, on_duplicate_key_update: [:name])
27
+ assert User.count == updated_users.length
28
+ User.all.each_with_index do |user, i|
29
+ assert_equal user.name, users[i].name + ' Rothschild'
30
+ assert_equal 1, user.lock_version
31
+ end
32
+ end
33
+
34
+ it 'lock_version upsert after on_duplcate_key_update by array' do
35
+ users = [
36
+ User.new(name: 'Salomon'),
37
+ User.new(name: 'Nathan')
38
+ ]
39
+ User.import(users)
40
+ assert User.count == users.length
41
+ User.all.each do |user|
42
+ assert_equal 0, user.lock_version
43
+ end
44
+
45
+ columns = [:id, :name]
46
+ updated_values = User.all.map do |user|
47
+ user.name += ' Rothschild'
48
+ [user.id, user.name]
49
+ end
50
+ User.import(columns, updated_values, on_duplicate_key_update: [:name])
51
+ assert User.count == updated_values.length
52
+ User.all.each_with_index do |user, i|
53
+ assert_equal user.name, users[i].name + ' Rothschild'
54
+ assert_equal 1, user.lock_version
55
+ end
56
+ end
57
+
58
+ it 'lock_version upsert after on_duplcate_key_update by hash' do
59
+ users = [
60
+ User.new(name: 'Salomon'),
61
+ User.new(name: 'Nathan')
62
+ ]
63
+ User.import(users)
64
+ assert User.count == users.length
65
+ User.all.each do |user|
66
+ assert_equal 0, user.lock_version
67
+ end
68
+ updated_values = User.all.map do |user|
69
+ user.name += ' Rothschild'
70
+ { id: user.id, name: user.name }
71
+ end
72
+ User.import(updated_values, on_duplicate_key_update: [:name])
73
+ assert User.count == updated_values.length
74
+ User.all.each_with_index do |user, i|
75
+ assert_equal user.name, users[i].name + ' Rothschild'
76
+ assert_equal 1, user.lock_version
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
88
+ end
89
+
90
+ it 'upsert optimistic lock columns other than lock_version by model' do
91
+ accounts = [
92
+ Account.new(name: 'Salomon'),
93
+ Account.new(name: 'Nathan')
94
+ ]
95
+ Account.import(accounts)
96
+ assert Account.count == accounts.length
97
+ Account.all.each do |user|
98
+ assert_equal 0, user.lock
99
+ end
100
+ updated_accounts = Account.all.map do |user|
101
+ user.name += ' Rothschild'
102
+ user
103
+ end
104
+ Account.import(updated_accounts, on_duplicate_key_update: [:id, :name])
105
+ assert Account.count == updated_accounts.length
106
+ Account.all.each_with_index do |user, i|
107
+ assert_equal user.name, accounts[i].name + ' Rothschild'
108
+ assert_equal 1, user.lock
109
+ end
110
+ end
111
+
112
+ it 'upsert optimistic lock columns other than lock_version by array' do
113
+ accounts = [
114
+ Account.new(name: 'Salomon'),
115
+ Account.new(name: 'Nathan')
116
+ ]
117
+ Account.import(accounts)
118
+ assert Account.count == accounts.length
119
+ Account.all.each do |user|
120
+ assert_equal 0, user.lock
121
+ end
122
+
123
+ columns = [:id, :name]
124
+ updated_values = Account.all.map do |user|
125
+ user.name += ' Rothschild'
126
+ [user.id, user.name]
127
+ end
128
+ Account.import(columns, updated_values, on_duplicate_key_update: [:name])
129
+ assert Account.count == updated_values.length
130
+ Account.all.each_with_index do |user, i|
131
+ assert_equal user.name, accounts[i].name + ' Rothschild'
132
+ assert_equal 1, user.lock
133
+ end
134
+ end
135
+
136
+ it 'upsert optimistic lock columns other than lock_version by hash' do
137
+ accounts = [
138
+ Account.new(name: 'Salomon'),
139
+ Account.new(name: 'Nathan')
140
+ ]
141
+ Account.import(accounts)
142
+ assert Account.count == accounts.length
143
+ Account.all.each do |user|
144
+ assert_equal 0, user.lock
145
+ end
146
+ updated_values = Account.all.map do |user|
147
+ user.name += ' Rothschild'
148
+ { id: user.id, name: user.name }
149
+ end
150
+ Account.import(updated_values, on_duplicate_key_update: [:name])
151
+ assert Account.count == updated_values.length
152
+ Account.all.each_with_index do |user, i|
153
+ assert_equal user.name, accounts[i].name + ' Rothschild'
154
+ assert_equal 1, user.lock
155
+ end
156
+ end
157
+
158
+ it 'update the lock_version of models separated by namespaces by model' do
159
+ makers = [
160
+ Bike::Maker.new(name: 'Yamaha'),
161
+ Bike::Maker.new(name: 'Honda')
162
+ ]
163
+ Bike::Maker.import(makers)
164
+ assert Bike::Maker.count == makers.length
165
+ Bike::Maker.all.each do |maker|
166
+ assert_equal 0, maker.lock_version
167
+ end
168
+ updated_makers = Bike::Maker.all.map do |maker|
169
+ maker.name += ' bikes'
170
+ maker
171
+ end
172
+ Bike::Maker.import(updated_makers, on_duplicate_key_update: [:name])
173
+ assert Bike::Maker.count == updated_makers.length
174
+ Bike::Maker.all.each_with_index do |maker, i|
175
+ assert_equal maker.name, makers[i].name + ' bikes'
176
+ assert_equal 1, maker.lock_version
177
+ end
178
+ end
179
+ it 'update the lock_version of models separated by namespaces by array' do
180
+ makers = [
181
+ Bike::Maker.new(name: 'Yamaha'),
182
+ Bike::Maker.new(name: 'Honda')
183
+ ]
184
+ Bike::Maker.import(makers)
185
+ assert Bike::Maker.count == makers.length
186
+ Bike::Maker.all.each do |maker|
187
+ assert_equal 0, maker.lock_version
188
+ end
189
+
190
+ columns = [:id, :name]
191
+ updated_values = Bike::Maker.all.map do |maker|
192
+ maker.name += ' bikes'
193
+ [maker.id, maker.name]
194
+ end
195
+ Bike::Maker.import(columns, updated_values, on_duplicate_key_update: [:name])
196
+ assert Bike::Maker.count == updated_values.length
197
+ Bike::Maker.all.each_with_index do |maker, i|
198
+ assert_equal maker.name, makers[i].name + ' bikes'
199
+ assert_equal 1, maker.lock_version
200
+ end
201
+ end
202
+
203
+ it 'update the lock_version of models separated by namespaces by hash' do
204
+ makers = [
205
+ Bike::Maker.new(name: 'Yamaha'),
206
+ Bike::Maker.new(name: 'Honda')
207
+ ]
208
+ Bike::Maker.import(makers)
209
+ assert Bike::Maker.count == makers.length
210
+ Bike::Maker.all.each do |maker|
211
+ assert_equal 0, maker.lock_version
212
+ end
213
+ updated_values = Bike::Maker.all.map do |maker|
214
+ maker.name += ' bikes'
215
+ { id: maker.id, name: maker.name }
216
+ end
217
+ Bike::Maker.import(updated_values, on_duplicate_key_update: [:name])
218
+ assert Bike::Maker.count == updated_values.length
219
+ Bike::Maker.all.each_with_index do |maker, i|
220
+ assert_equal maker.name, makers[i].name + ' bikes'
221
+ assert_equal 1, maker.lock_version
222
+ end
223
+ end
224
+ end
225
+ end
226
+
8
227
  context "with :on_duplicate_key_update" do
228
+ describe 'using :all' do
229
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
230
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
231
+
232
+ macro(:perform_import) do |*opts|
233
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: :all, validate: false)
234
+ end
235
+
236
+ setup do
237
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
238
+ Topic.import columns + ['replies_count'], values, validate: false
239
+ end
240
+
241
+ it 'updates all specified columns' do
242
+ perform_import
243
+ updated_topic = Topic.find(99)
244
+ assert_equal 'Book - 2nd Edition', updated_topic.title
245
+ assert_equal 'Jane Doe', updated_topic.author_name
246
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
247
+ assert_equal 57, updated_topic.parent_id
248
+ assert_equal 3, updated_topic.replies_count
249
+ end
250
+ end
251
+
9
252
  describe "argument safety" do
10
253
  it "should not modify the passed in :on_duplicate_key_update array" do
11
254
  assert_nothing_raised do
@@ -15,6 +258,26 @@ def should_support_basic_on_duplicate_key_update
15
258
  end
16
259
  end
17
260
 
261
+ context "with timestamps enabled" do
262
+ let(:time) { Chronic.parse("5 minutes from now") }
263
+
264
+ it 'should not overwrite changed updated_at with current timestamp' do
265
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
266
+ timestamp = Time.now.utc
267
+ topic.updated_at = timestamp
268
+ Topic.import [topic], on_duplicate_key_update: :all, validate: false
269
+ assert_equal timestamp.to_s, Topic.last.updated_at.to_s
270
+ end
271
+
272
+ it 'should update updated_at with current timestamp' do
273
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
274
+ Timecop.freeze(time) do
275
+ Topic.import [topic], on_duplicate_key_update: [:updated_at], validate: false
276
+ assert_in_delta time.to_i, topic.reload.updated_at.to_i, 1.second
277
+ end
278
+ end
279
+ end
280
+
18
281
  context "with validation checks turned off" do
19
282
  asssertion_group(:should_support_on_duplicate_key_update) do
20
283
  should_not_update_fields_not_mentioned
@@ -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
@@ -116,7 +118,10 @@ def should_support_recursive_import
116
118
  end
117
119
 
118
120
  it "imports an imported belongs_to association id" do
119
- books = new_topics[0].books.to_a
121
+ first_new_topic = new_topics[0]
122
+ second_new_topic = new_topics[1]
123
+
124
+ books = first_new_topic.books.to_a
120
125
  Topic.import new_topics, validate: false
121
126
 
122
127
  assert_difference "Book.count", books.size do
@@ -124,7 +129,25 @@ def should_support_recursive_import
124
129
  end
125
130
 
126
131
  books.each do |book|
127
- assert_not_nil book.topic_id
132
+ assert_equal book.topic_id, first_new_topic.id
133
+ end
134
+
135
+ books.each { |book| book.topic_id = second_new_topic.id }
136
+ assert_no_difference "Book.count", books.size do
137
+ Book.import books, validate: false, on_duplicate_key_update: [:topic_id]
138
+ end
139
+
140
+ books.each do |book|
141
+ assert_equal book.topic_id, second_new_topic.id
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_equal book.topic_id, nil
128
151
  end
129
152
  end
130
153
 
@@ -155,7 +178,7 @@ def should_support_recursive_import
155
178
  end
156
179
  end
157
180
 
158
- # If adapter supports on_duplicate_key_update, it is only applied to top level models so that SQL with invalid
181
+ # 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
159
182
  # columns, keys, etc isn't generated for child associations when doing recursive import
160
183
  if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
161
184
  describe "on_duplicate_key_update" do
@@ -169,6 +192,55 @@ def should_support_recursive_import
169
192
  end
170
193
  end
171
194
  end
195
+
196
+ context "when :all fields are updated" do
197
+ setup do
198
+ Topic.import new_topics, recursive: true
199
+ end
200
+
201
+ it "updates associated objects" do
202
+ new_author_name = 'Richard Bachman'
203
+ topic = new_topics.first
204
+ topic.books.each do |book|
205
+ book.author_name = new_author_name
206
+ end
207
+ assert_nothing_raised do
208
+ Topic.import new_topics, recursive: true, on_duplicate_key_update: :all
209
+ end
210
+ Topic.find(topic.id).books.each do |book|
211
+ assert_equal new_author_name, book.author_name
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ # If returning option is provided, it is only applied to top level models so that SQL with invalid
219
+ # columns, keys, etc isn't generated for child associations when doing recursive import
220
+ describe "returning" do
221
+ let(:new_topics) { Build(1, :topic_with_book) }
222
+
223
+ it "imports objects with associations" do
224
+ assert_difference "Topic.count", +1 do
225
+ Topic.import new_topics, recursive: true, returning: [:content], validate: false
226
+ new_topics.each do |topic|
227
+ assert_not_nil topic.id
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ # If no returning option is provided, it is ignored
234
+ describe "no returning" do
235
+ let(:new_topics) { Build(1, :topic_with_book) }
236
+
237
+ it "is ignored and imports objects with associations" do
238
+ assert_difference "Topic.count", +1 do
239
+ Topic.import new_topics, recursive: true, no_returning: true, validate: false
240
+ new_topics.each do |topic|
241
+ assert_not_nil topic.id
242
+ end
243
+ end
172
244
  end
173
245
  end
174
246
  end
@@ -1,23 +1,13 @@
1
- # encoding: UTF-8
1
+ # frozen_string_literal: true
2
+
2
3
  def should_support_sqlite3_import_functionality
3
- should_support_on_duplicate_key_ignore
4
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
5
+ should_support_sqlite_upsert_functionality
6
+ end
4
7
 
5
8
  describe "#supports_imports?" do
6
- context "and SQLite is 3.7.11 or higher" do
7
- it "supports import" do
8
- version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new("3.7.11")
9
- assert ActiveRecord::Base.supports_import?(version)
10
-
11
- version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new("3.7.12")
12
- assert ActiveRecord::Base.supports_import?(version)
13
- end
14
- end
15
-
16
- context "and SQLite less than 3.7.11" do
17
- it "doesn't support import" do
18
- version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new("3.7.10")
19
- assert !ActiveRecord::Base.supports_import?(version)
20
- end
9
+ it "should support import" do
10
+ assert ActiveRecord::Base.supports_import?
21
11
  end
22
12
  end
23
13
 
@@ -49,18 +39,193 @@ def should_support_sqlite3_import_functionality
49
39
  assert_equal 2500, Topic.count, "Failed to insert all records. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed"
50
40
  end
51
41
  end
42
+ end
43
+ end
44
+
45
+ def should_support_sqlite_upsert_functionality
46
+ should_support_basic_on_duplicate_key_update
47
+ should_support_on_duplicate_key_ignore
48
+
49
+ describe "#import" do
50
+ extend ActiveSupport::TestCase::ImportAssertions
51
+
52
+ macro(:perform_import) { raise "supply your own #perform_import in a context below" }
53
+ macro(:updated_topic) { Topic.find(@topic.id) }
54
+
55
+ context "with :on_duplicate_key_ignore and validation checks turned off" do
56
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
57
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
58
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
59
+
60
+ setup do
61
+ Topic.import columns, values, validate: false
62
+ end
63
+
64
+ it "should not update any records" do
65
+ result = Topic.import columns, updated_values, on_duplicate_key_ignore: true, validate: false
66
+ assert_equal [], result.ids
67
+ end
68
+ end
69
+
70
+ context "with :on_duplicate_key_update and validation checks turned off" do
71
+ asssertion_group(:should_support_on_duplicate_key_update) do
72
+ should_not_update_fields_not_mentioned
73
+ should_update_foreign_keys
74
+ should_not_update_created_at_on_timestamp_columns
75
+ should_update_updated_at_on_timestamp_columns
76
+ end
77
+
78
+ context "using a hash" do
79
+ context "with :columns a hash" do
80
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
81
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
82
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
83
+
84
+ macro(:perform_import) do |*opts|
85
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: update_columns }, validate: false)
86
+ end
87
+
88
+ setup do
89
+ Topic.import columns, values, validate: false
90
+ @topic = Topic.find 99
91
+ end
92
+
93
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
94
+ assert_nothing_raised do
95
+ columns = %w(title author_name).freeze
96
+ Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: { columns: columns }
97
+ end
98
+ end
99
+
100
+ context "using string hash map" do
101
+ let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
102
+ should_support_on_duplicate_key_update
103
+ should_update_fields_mentioned
104
+ end
105
+
106
+ context "using string hash map, but specifying column mismatches" do
107
+ let(:update_columns) { { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
108
+ should_support_on_duplicate_key_update
109
+ should_update_fields_mentioned_with_hash_mappings
110
+ end
111
+
112
+ context "using symbol hash map" do
113
+ let(:update_columns) { { title: :title, author_email_address: :author_email_address, parent_id: :parent_id } }
114
+ should_support_on_duplicate_key_update
115
+ should_update_fields_mentioned
116
+ end
117
+
118
+ context "using symbol hash map, but specifying column mismatches" do
119
+ let(:update_columns) { { title: :author_email_address, author_email_address: :title, parent_id: :parent_id } }
120
+ should_support_on_duplicate_key_update
121
+ should_update_fields_mentioned_with_hash_mappings
122
+ end
123
+ end
124
+
125
+ context 'with :index_predicate' do
126
+ let(:columns) { %w( id device_id alarm_type status metadata ) }
127
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
128
+ let(:updated_values) { [[99, 17, 1, 2, 'bar']] }
129
+
130
+ macro(:perform_import) do |*opts|
131
+ 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)
132
+ end
133
+
134
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
135
+
136
+ setup do
137
+ Alarm.import columns, values, validate: false
138
+ @alarm = Alarm.find 99
139
+ end
140
+
141
+ context 'supports on duplicate key update for partial indexes' do
142
+ it 'should not update created_at timestamp columns' do
143
+ Timecop.freeze Chronic.parse("5 minutes from now") do
144
+ perform_import
145
+ assert_in_delta @alarm.created_at.to_i, updated_alarm.created_at.to_i, 1
146
+ end
147
+ end
148
+
149
+ it 'should update updated_at timestamp columns' do
150
+ time = Chronic.parse("5 minutes from now")
151
+ Timecop.freeze time do
152
+ perform_import
153
+ assert_in_delta time.to_i, updated_alarm.updated_at.to_i, 1
154
+ end
155
+ end
156
+
157
+ it 'should not update fields not mentioned' do
158
+ perform_import
159
+ assert_equal 'foo', updated_alarm.metadata
160
+ end
161
+
162
+ it 'should update fields mentioned with hash mappings' do
163
+ perform_import
164
+ assert_equal 2, updated_alarm.status
165
+ end
166
+ end
167
+ end
168
+
169
+ context 'with :condition' do
170
+ let(:columns) { %w( id device_id alarm_type status metadata) }
171
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
172
+ let(:updated_values) { [[99, 17, 1, 1, 'bar']] }
173
+
174
+ macro(:perform_import) do |*opts|
175
+ Alarm.import(
176
+ columns,
177
+ updated_values,
178
+ opts.extract_options!.merge(
179
+ on_duplicate_key_update: {
180
+ conflict_target: [:id],
181
+ condition: "alarms.metadata NOT LIKE '%foo%'",
182
+ columns: [:metadata]
183
+ },
184
+ validate: false
185
+ )
186
+ )
187
+ end
188
+
189
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
190
+
191
+ setup do
192
+ Alarm.import columns, values, validate: false
193
+ @alarm = Alarm.find 99
194
+ end
195
+
196
+ it 'should not update fields not matched' do
197
+ perform_import
198
+ assert_equal 'foo', updated_alarm.metadata
199
+ end
200
+ end
201
+
202
+ context "with no :conflict_target" do
203
+ context "with no primary key" do
204
+ it "raises ArgumentError" do
205
+ error = assert_raises ArgumentError do
206
+ Rule.import Build(3, :rules), on_duplicate_key_update: [:condition_text], validate: false
207
+ end
208
+ assert_match(/Expected :conflict_target to be specified/, error.message)
209
+ end
210
+ end
211
+ end
212
+
213
+ context "with no :columns" do
214
+ let(:columns) { %w( id title author_name author_email_address ) }
215
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com"]] }
216
+ let(:updated_values) { [[100, "Title Should Not Change", "Author Should Not Change", "john@nogo.com"]] }
52
217
 
53
- context "with :on_duplicate_key_update" do
54
- let(:topics) { Build(1, :topics) }
218
+ macro(:perform_import) do |*opts|
219
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id }, validate: false)
220
+ end
55
221
 
56
- it "should log a warning message" do
57
- log = StringIO.new
58
- logger = Logger.new(log)
59
- logger.level = Logger::WARN
60
- ActiveRecord::Base.connection.stubs(:logger).returns(logger)
222
+ setup do
223
+ Topic.import columns, values, validate: false
224
+ @topic = Topic.find 100
225
+ end
61
226
 
62
- Topic.import topics, on_duplicate_key_update: true
63
- assert_match(/Ignoring on_duplicate_key_update/, log.string)
227
+ should_update_updated_at_on_timestamp_columns
228
+ end
64
229
  end
65
230
  end
66
231
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path('../test_helper', __FILE__)
2
4
 
3
5
  describe ".synchronize" do