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.
- checksums.yaml +5 -5
- data/.github/workflows/test.yaml +107 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +74 -8
- data/Brewfile +3 -1
- data/CHANGELOG.md +235 -3
- data/Gemfile +22 -15
- data/LICENSE +21 -56
- data/README.markdown +574 -22
- data/Rakefile +4 -1
- data/activerecord-import.gemspec +6 -5
- data/benchmarks/benchmark.rb +7 -1
- data/benchmarks/lib/base.rb +2 -0
- data/benchmarks/lib/cli_parser.rb +3 -1
- data/benchmarks/lib/float.rb +2 -0
- data/benchmarks/lib/mysql2_benchmark.rb +2 -0
- data/benchmarks/lib/output_to_csv.rb +2 -0
- data/benchmarks/lib/output_to_html.rb +4 -2
- data/benchmarks/models/test_innodb.rb +2 -0
- data/benchmarks/models/test_memory.rb +2 -0
- data/benchmarks/models/test_myisam.rb +2 -0
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
- data/gemfiles/4.2.gemfile +2 -0
- data/gemfiles/5.0.gemfile +2 -0
- data/gemfiles/5.1.gemfile +2 -0
- data/gemfiles/5.2.gemfile +4 -0
- data/gemfiles/6.0.gemfile +4 -0
- data/gemfiles/6.1.gemfile +4 -0
- data/gemfiles/7.0.gemfile +4 -0
- data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
- data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +10 -2
- data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +19 -11
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +56 -37
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
- data/lib/activerecord-import/base.rb +12 -2
- data/lib/activerecord-import/import.rb +300 -136
- data/lib/activerecord-import/mysql2.rb +2 -0
- data/lib/activerecord-import/postgresql.rb +2 -0
- data/lib/activerecord-import/sqlite3.rb +2 -0
- data/lib/activerecord-import/synchronize.rb +4 -2
- data/lib/activerecord-import/value_sets_parser.rb +4 -0
- data/lib/activerecord-import/version.rb +3 -1
- data/lib/activerecord-import.rb +4 -1
- data/test/adapters/jdbcmysql.rb +2 -0
- data/test/adapters/jdbcpostgresql.rb +2 -0
- data/test/adapters/jdbcsqlite3.rb +2 -0
- data/test/adapters/makara_postgis.rb +3 -0
- data/test/adapters/mysql2.rb +2 -0
- data/test/adapters/mysql2_makara.rb +2 -0
- data/test/adapters/mysql2spatial.rb +2 -0
- data/test/adapters/postgis.rb +2 -0
- data/test/adapters/postgresql.rb +2 -0
- data/test/adapters/postgresql_makara.rb +2 -0
- data/test/adapters/seamless_database_pool.rb +2 -0
- data/test/adapters/spatialite.rb +2 -0
- data/test/adapters/sqlite3.rb +2 -0
- data/test/{travis → github}/database.yml +3 -1
- data/test/import_test.rb +159 -8
- data/test/jdbcmysql/import_test.rb +2 -0
- data/test/jdbcpostgresql/import_test.rb +2 -0
- data/test/jdbcsqlite3/import_test.rb +2 -0
- data/test/makara_postgis/import_test.rb +10 -0
- data/test/models/account.rb +5 -0
- data/test/models/alarm.rb +2 -0
- data/test/models/animal.rb +8 -0
- data/test/models/bike_maker.rb +9 -0
- data/test/models/book.rb +2 -0
- data/test/models/car.rb +2 -0
- data/test/models/card.rb +5 -0
- data/test/models/chapter.rb +2 -0
- data/test/models/customer.rb +8 -0
- data/test/models/deck.rb +8 -0
- data/test/models/dictionary.rb +2 -0
- data/test/models/discount.rb +2 -0
- data/test/models/end_note.rb +2 -0
- data/test/models/group.rb +2 -0
- data/test/models/order.rb +8 -0
- data/test/models/playing_card.rb +4 -0
- data/test/models/promotion.rb +2 -0
- data/test/models/question.rb +2 -0
- data/test/models/rule.rb +2 -0
- data/test/models/tag.rb +3 -0
- data/test/models/tag_alias.rb +5 -0
- data/test/models/topic.rb +2 -0
- data/test/models/user.rb +5 -0
- data/test/models/user_token.rb +6 -0
- data/test/models/vendor.rb +2 -0
- data/test/models/widget.rb +2 -0
- data/test/mysql2/import_test.rb +2 -0
- data/test/mysql2_makara/import_test.rb +2 -0
- data/test/mysqlspatial2/import_test.rb +2 -0
- data/test/postgis/import_test.rb +2 -0
- data/test/postgresql/import_test.rb +2 -0
- data/test/schema/generic_schema.rb +53 -0
- data/test/schema/jdbcpostgresql_schema.rb +2 -0
- data/test/schema/mysql2_schema.rb +21 -0
- data/test/schema/postgis_schema.rb +2 -0
- data/test/schema/postgresql_schema.rb +18 -0
- data/test/schema/sqlite3_schema.rb +15 -0
- data/test/schema/version.rb +2 -0
- data/test/sqlite3/import_test.rb +2 -0
- data/test/support/active_support/test_case_extensions.rb +2 -0
- data/test/support/assertions.rb +2 -0
- data/test/support/factories.rb +10 -8
- data/test/support/generate.rb +10 -8
- data/test/support/mysql/import_examples.rb +14 -1
- data/test/support/postgresql/import_examples.rb +140 -3
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +263 -0
- data/test/support/shared_examples/recursive_import.rb +76 -4
- data/test/support/sqlite3/import_examples.rb +191 -26
- data/test/synchronize_test.rb +2 -0
- data/test/test_helper.rb +36 -3
- data/test/value_sets_bytes_parser_test.rb +2 -0
- data/test/value_sets_records_parser_test.rb +2 -0
- metadata +46 -18
- data/.travis.yml +0 -61
- data/gemfiles/3.2.gemfile +0 -2
- data/gemfiles/4.0.gemfile +0 -2
- data/gemfiles/4.1.gemfile +0 -2
- 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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
def should_support_sqlite3_import_functionality
|
|
3
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
227
|
+
should_update_updated_at_on_timestamp_columns
|
|
228
|
+
end
|
|
64
229
|
end
|
|
65
230
|
end
|
|
66
231
|
end
|