activerecord-import 0.25.0 → 1.7.0
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 +151 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +74 -8
- data/.rubocop_todo.yml +10 -16
- data/Brewfile +3 -1
- data/CHANGELOG.md +232 -2
- data/Dockerfile +23 -0
- data/Gemfile +26 -14
- data/LICENSE +21 -56
- data/README.markdown +612 -21
- data/Rakefile +4 -1
- data/activerecord-import.gemspec +6 -5
- data/benchmarks/benchmark.rb +10 -4
- data/benchmarks/lib/base.rb +4 -2
- data/benchmarks/lib/cli_parser.rb +4 -2
- 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/docker-compose.yml +34 -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 +2 -0
- data/gemfiles/6.0.gemfile +4 -0
- data/gemfiles/6.1.gemfile +4 -0
- data/gemfiles/7.0.gemfile +4 -0
- data/gemfiles/7.1.gemfile +3 -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/active_record/adapters/trilogy_adapter.rb +8 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +15 -6
- 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 +34 -29
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +74 -55
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +138 -13
- data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
- data/lib/activerecord-import/base.rb +11 -2
- data/lib/activerecord-import/import.rb +290 -114
- 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 +5 -0
- data/lib/activerecord-import/version.rb +3 -1
- data/lib/activerecord-import.rb +2 -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 +2 -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/adapters/trilogy.rb +9 -0
- data/test/database.yml.sample +7 -0
- data/test/{travis → github}/database.yml +7 -1
- data/test/import_test.rb +151 -8
- data/test/jdbcmysql/import_test.rb +5 -3
- data/test/jdbcpostgresql/import_test.rb +4 -2
- data/test/jdbcsqlite3/import_test.rb +4 -2
- data/test/makara_postgis/import_test.rb +4 -2
- data/test/models/account.rb +2 -0
- data/test/models/alarm.rb +2 -0
- data/test/models/animal.rb +8 -0
- data/test/models/author.rb +7 -0
- data/test/models/bike_maker.rb +3 -0
- data/test/models/book.rb +7 -2
- data/test/models/car.rb +2 -0
- data/test/models/card.rb +5 -0
- data/test/models/chapter.rb +2 -0
- data/test/models/composite_book.rb +19 -0
- data/test/models/composite_chapter.rb +9 -0
- data/test/models/customer.rb +18 -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 +17 -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 +9 -1
- data/test/models/tag_alias.rb +11 -0
- data/test/models/topic.rb +7 -0
- data/test/models/user.rb +2 -0
- data/test/models/user_token.rb +3 -0
- data/test/models/vendor.rb +2 -0
- data/test/models/widget.rb +2 -0
- data/test/mysql2/import_test.rb +5 -3
- data/test/mysql2_makara/import_test.rb +5 -3
- data/test/mysqlspatial2/import_test.rb +5 -3
- data/test/postgis/import_test.rb +4 -2
- data/test/postgresql/import_test.rb +4 -2
- data/test/schema/generic_schema.rb +37 -1
- data/test/schema/jdbcpostgresql_schema.rb +3 -1
- data/test/schema/mysql2_schema.rb +2 -0
- data/test/schema/postgis_schema.rb +3 -1
- data/test/schema/postgresql_schema.rb +49 -0
- data/test/schema/sqlite3_schema.rb +15 -0
- data/test/schema/version.rb +2 -0
- data/test/sqlite3/import_test.rb +4 -2
- 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 +2 -1
- data/test/support/postgresql/import_examples.rb +152 -3
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +122 -9
- data/test/support/shared_examples/recursive_import.rb +128 -2
- data/test/support/sqlite3/import_examples.rb +191 -26
- data/test/synchronize_test.rb +2 -0
- data/test/test_helper.rb +34 -7
- data/test/trilogy/import_test.rb +7 -0
- data/test/value_sets_bytes_parser_test.rb +3 -1
- data/test/value_sets_records_parser_test.rb +3 -1
- metadata +46 -16
- data/.travis.yml +0 -71
- data/gemfiles/3.2.gemfile +0 -2
- data/gemfiles/4.0.gemfile +0 -2
- data/gemfiles/4.1.gemfile +0 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
def should_support_postgresql_import_functionality
|
|
3
4
|
should_support_recursive_import
|
|
4
5
|
|
|
@@ -37,6 +38,12 @@ def should_support_postgresql_import_functionality
|
|
|
37
38
|
assert !topic.changed?
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
if ENV['AR_VERSION'].to_f > 4.1
|
|
42
|
+
it "moves the dirty changes to previous_changes" do
|
|
43
|
+
assert topic.previous_changes.present?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
40
47
|
it "marks models as persisted" do
|
|
41
48
|
assert !topic.new_record?
|
|
42
49
|
assert topic.persisted?
|
|
@@ -96,6 +103,8 @@ def should_support_postgresql_import_functionality
|
|
|
96
103
|
books.first.id.to_s
|
|
97
104
|
end
|
|
98
105
|
end
|
|
106
|
+
let(:true_returning_value) { ENV['AR_VERSION'].to_f >= 5.0 ? true : 't' }
|
|
107
|
+
let(:false_returning_value) { ENV['AR_VERSION'].to_f >= 5.0 ? false : 'f' }
|
|
99
108
|
|
|
100
109
|
it "creates records" do
|
|
101
110
|
assert_difference("Book.count", +1) { result }
|
|
@@ -110,6 +119,26 @@ def should_support_postgresql_import_functionality
|
|
|
110
119
|
assert_equal [%w(King It)], result.results
|
|
111
120
|
end
|
|
112
121
|
|
|
122
|
+
context "when given an empty array" do
|
|
123
|
+
let(:result) { Book.import([], returning: %w(title)) }
|
|
124
|
+
|
|
125
|
+
setup { result }
|
|
126
|
+
|
|
127
|
+
it "returns empty arrays for ids and results" do
|
|
128
|
+
assert_equal [], result.ids
|
|
129
|
+
assert_equal [], result.results
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
context "when a returning column is a serialized attribute" do
|
|
134
|
+
let(:vendor) { Vendor.new(hours: { monday: '8-5' }) }
|
|
135
|
+
let(:result) { Vendor.import([vendor], returning: %w(hours)) }
|
|
136
|
+
|
|
137
|
+
it "creates records" do
|
|
138
|
+
assert_difference("Vendor.count", +1) { result }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
113
142
|
context "when primary key and returning overlap" do
|
|
114
143
|
let(:result) { Book.import(books, returning: %w(id title)) }
|
|
115
144
|
|
|
@@ -124,6 +153,34 @@ def should_support_postgresql_import_functionality
|
|
|
124
153
|
end
|
|
125
154
|
end
|
|
126
155
|
|
|
156
|
+
context "when returning is raw sql" do
|
|
157
|
+
let(:result) { Book.import(books, returning: "title, (xmax = '0') AS inserted") }
|
|
158
|
+
|
|
159
|
+
setup { result }
|
|
160
|
+
|
|
161
|
+
it "returns ids" do
|
|
162
|
+
assert_equal [book_id], result.ids
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "returns specified columns" do
|
|
166
|
+
assert_equal [['It', true_returning_value]], result.results
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
context "when returning contains raw sql" do
|
|
171
|
+
let(:result) { Book.import(books, returning: [:title, "id, (xmax = '0') AS inserted"]) }
|
|
172
|
+
|
|
173
|
+
setup { result }
|
|
174
|
+
|
|
175
|
+
it "returns ids" do
|
|
176
|
+
assert_equal [book_id], result.ids
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "returns specified columns" do
|
|
180
|
+
assert_equal [['It', book_id, true_returning_value]], result.results
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
127
184
|
context "setting model attributes" do
|
|
128
185
|
let(:code) { 'abc' }
|
|
129
186
|
let(:discount) { 0.10 }
|
|
@@ -153,6 +210,14 @@ def should_support_postgresql_import_functionality
|
|
|
153
210
|
assert_equal updated_promotion.discount, discount
|
|
154
211
|
end
|
|
155
212
|
end
|
|
213
|
+
|
|
214
|
+
context 'returning raw sql' do
|
|
215
|
+
let(:returning_columns) { [:discount, "(xmax = '0') AS inserted"] }
|
|
216
|
+
|
|
217
|
+
it "sets custom model attributes" do
|
|
218
|
+
assert_equal updated_promotion.inserted, false_returning_value
|
|
219
|
+
end
|
|
220
|
+
end
|
|
156
221
|
end
|
|
157
222
|
end
|
|
158
223
|
end
|
|
@@ -228,10 +293,34 @@ def should_support_postgresql_import_functionality
|
|
|
228
293
|
assert_equal({}, Vendor.first.json_data)
|
|
229
294
|
end
|
|
230
295
|
end
|
|
296
|
+
|
|
297
|
+
%w(json jsonb).each do |json_type|
|
|
298
|
+
describe "with pure #{json_type} fields" do
|
|
299
|
+
let(:data) { { a: :b } }
|
|
300
|
+
let(:json_field_name) { "pure_#{json_type}_data" }
|
|
301
|
+
it "imports the values from saved records" do
|
|
302
|
+
vendor = Vendor.create!(name: 'Vendor 1', json_field_name => data)
|
|
303
|
+
|
|
304
|
+
Vendor.import [vendor], on_duplicate_key_update: [json_field_name]
|
|
305
|
+
assert_equal(data.as_json, vendor.reload[json_field_name])
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
describe "with enum field" do
|
|
312
|
+
let(:vendor_type) { "retailer" }
|
|
313
|
+
it "imports the correct values for enum fields" do
|
|
314
|
+
vendor = Vendor.new(name: 'Vendor 1', vendor_type: vendor_type)
|
|
315
|
+
assert_difference "Vendor.count", +1 do
|
|
316
|
+
Vendor.import [vendor]
|
|
317
|
+
end
|
|
318
|
+
assert_equal(vendor_type, Vendor.first.vendor_type)
|
|
319
|
+
end
|
|
231
320
|
end
|
|
232
321
|
|
|
233
322
|
describe "with binary field" do
|
|
234
|
-
let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".force_encoding('ASCII-8BIT') }
|
|
323
|
+
let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".dup.force_encoding('ASCII-8BIT') }
|
|
235
324
|
it "imports the correct values for binary fields" do
|
|
236
325
|
alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
|
|
237
326
|
assert_difference "Alarm.count", +1 do
|
|
@@ -240,6 +329,42 @@ def should_support_postgresql_import_functionality
|
|
|
240
329
|
assert_equal(binary_value, Alarm.first.secret_key)
|
|
241
330
|
end
|
|
242
331
|
end
|
|
332
|
+
|
|
333
|
+
unless ENV["SKIP_COMPOSITE_PK"]
|
|
334
|
+
describe "with composite foreign keys" do
|
|
335
|
+
let(:account_id) { 555 }
|
|
336
|
+
let(:customer) { Customer.new(account_id: account_id, name: "foo") }
|
|
337
|
+
let(:order) { Order.new(account_id: account_id, amount: 100, customer: customer) }
|
|
338
|
+
|
|
339
|
+
it "imports and correctly maps foreign keys" do
|
|
340
|
+
assert_difference "Customer.count", +1 do
|
|
341
|
+
Customer.import [customer]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
assert_difference "Order.count", +1 do
|
|
345
|
+
Order.import [order]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
db_customer = Customer.last
|
|
349
|
+
db_order = Order.last
|
|
350
|
+
|
|
351
|
+
assert_equal db_customer.orders.last, db_order
|
|
352
|
+
assert_not_equal db_order.customer_id, nil
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it "should import models with auto-incrementing ID successfully" do
|
|
356
|
+
author = Author.create!(name: "Foo Barson")
|
|
357
|
+
|
|
358
|
+
books = []
|
|
359
|
+
2.times do |i|
|
|
360
|
+
books << CompositeBook.new(author_id: author.id, title: "book #{i}")
|
|
361
|
+
end
|
|
362
|
+
assert_difference "CompositeBook.count", +2 do
|
|
363
|
+
CompositeBook.import books
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
243
368
|
end
|
|
244
369
|
|
|
245
370
|
def should_support_postgresql_upsert_functionality
|
|
@@ -295,6 +420,30 @@ def should_support_postgresql_upsert_functionality
|
|
|
295
420
|
end
|
|
296
421
|
|
|
297
422
|
context "using a hash" do
|
|
423
|
+
context "with :columns :all" do
|
|
424
|
+
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
425
|
+
let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
|
|
426
|
+
|
|
427
|
+
macro(:perform_import) do |*opts|
|
|
428
|
+
Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
setup do
|
|
432
|
+
values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
|
|
433
|
+
Topic.import columns + ['replies_count'], values, validate: false
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
it "should update all specified columns" do
|
|
437
|
+
perform_import
|
|
438
|
+
updated_topic = Topic.find(99)
|
|
439
|
+
assert_equal 'Book - 2nd Edition', updated_topic.title
|
|
440
|
+
assert_equal 'Jane Doe', updated_topic.author_name
|
|
441
|
+
assert_equal 'janedoe@example.com', updated_topic.author_email_address
|
|
442
|
+
assert_equal 57, updated_topic.parent_id
|
|
443
|
+
assert_equal 3, updated_topic.replies_count
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
298
447
|
context "with :columns a hash" do
|
|
299
448
|
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
300
449
|
let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
|
|
@@ -312,7 +461,7 @@ def should_support_postgresql_upsert_functionality
|
|
|
312
461
|
it "should not modify the passed in :on_duplicate_key_update columns array" do
|
|
313
462
|
assert_nothing_raised do
|
|
314
463
|
columns = %w(title author_name).freeze
|
|
315
|
-
Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: { columns: columns }
|
|
464
|
+
Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
|
|
316
465
|
end
|
|
317
466
|
end
|
|
318
467
|
|
|
@@ -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
|
|
@@ -24,7 +26,7 @@ def should_support_basic_on_duplicate_key_update
|
|
|
24
26
|
User.import(updated_users, on_duplicate_key_update: [:name])
|
|
25
27
|
assert User.count == updated_users.length
|
|
26
28
|
User.all.each_with_index do |user, i|
|
|
27
|
-
assert_equal user.name, users[i].name
|
|
29
|
+
assert_equal user.name, "#{users[i].name} Rothschild"
|
|
28
30
|
assert_equal 1, user.lock_version
|
|
29
31
|
end
|
|
30
32
|
end
|
|
@@ -48,7 +50,7 @@ def should_support_basic_on_duplicate_key_update
|
|
|
48
50
|
User.import(columns, updated_values, on_duplicate_key_update: [:name])
|
|
49
51
|
assert User.count == updated_values.length
|
|
50
52
|
User.all.each_with_index do |user, i|
|
|
51
|
-
assert_equal user.name, users[i].name
|
|
53
|
+
assert_equal user.name, "#{users[i].name} Rothschild"
|
|
52
54
|
assert_equal 1, user.lock_version
|
|
53
55
|
end
|
|
54
56
|
end
|
|
@@ -70,9 +72,19 @@ def should_support_basic_on_duplicate_key_update
|
|
|
70
72
|
User.import(updated_values, on_duplicate_key_update: [:name])
|
|
71
73
|
assert User.count == updated_values.length
|
|
72
74
|
User.all.each_with_index do |user, i|
|
|
73
|
-
assert_equal user.name, users[i].name
|
|
75
|
+
assert_equal user.name, "#{users[i].name} Rothschild"
|
|
74
76
|
assert_equal 1, user.lock_version
|
|
75
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
|
|
76
88
|
end
|
|
77
89
|
|
|
78
90
|
it 'upsert optimistic lock columns other than lock_version by model' do
|
|
@@ -92,7 +104,7 @@ def should_support_basic_on_duplicate_key_update
|
|
|
92
104
|
Account.import(updated_accounts, on_duplicate_key_update: [:id, :name])
|
|
93
105
|
assert Account.count == updated_accounts.length
|
|
94
106
|
Account.all.each_with_index do |user, i|
|
|
95
|
-
assert_equal user.name, accounts[i].name
|
|
107
|
+
assert_equal user.name, "#{accounts[i].name} Rothschild"
|
|
96
108
|
assert_equal 1, user.lock
|
|
97
109
|
end
|
|
98
110
|
end
|
|
@@ -116,7 +128,7 @@ def should_support_basic_on_duplicate_key_update
|
|
|
116
128
|
Account.import(columns, updated_values, on_duplicate_key_update: [:name])
|
|
117
129
|
assert Account.count == updated_values.length
|
|
118
130
|
Account.all.each_with_index do |user, i|
|
|
119
|
-
assert_equal user.name, accounts[i].name
|
|
131
|
+
assert_equal user.name, "#{accounts[i].name} Rothschild"
|
|
120
132
|
assert_equal 1, user.lock
|
|
121
133
|
end
|
|
122
134
|
end
|
|
@@ -138,7 +150,7 @@ def should_support_basic_on_duplicate_key_update
|
|
|
138
150
|
Account.import(updated_values, on_duplicate_key_update: [:name])
|
|
139
151
|
assert Account.count == updated_values.length
|
|
140
152
|
Account.all.each_with_index do |user, i|
|
|
141
|
-
assert_equal user.name, accounts[i].name
|
|
153
|
+
assert_equal user.name, "#{accounts[i].name} Rothschild"
|
|
142
154
|
assert_equal 1, user.lock
|
|
143
155
|
end
|
|
144
156
|
end
|
|
@@ -160,10 +172,11 @@ def should_support_basic_on_duplicate_key_update
|
|
|
160
172
|
Bike::Maker.import(updated_makers, on_duplicate_key_update: [:name])
|
|
161
173
|
assert Bike::Maker.count == updated_makers.length
|
|
162
174
|
Bike::Maker.all.each_with_index do |maker, i|
|
|
163
|
-
assert_equal maker.name, makers[i].name
|
|
175
|
+
assert_equal maker.name, "#{makers[i].name} bikes"
|
|
164
176
|
assert_equal 1, maker.lock_version
|
|
165
177
|
end
|
|
166
178
|
end
|
|
179
|
+
|
|
167
180
|
it 'update the lock_version of models separated by namespaces by array' do
|
|
168
181
|
makers = [
|
|
169
182
|
Bike::Maker.new(name: 'Yamaha'),
|
|
@@ -183,7 +196,7 @@ def should_support_basic_on_duplicate_key_update
|
|
|
183
196
|
Bike::Maker.import(columns, updated_values, on_duplicate_key_update: [:name])
|
|
184
197
|
assert Bike::Maker.count == updated_values.length
|
|
185
198
|
Bike::Maker.all.each_with_index do |maker, i|
|
|
186
|
-
assert_equal maker.name, makers[i].name
|
|
199
|
+
assert_equal maker.name, "#{makers[i].name} bikes"
|
|
187
200
|
assert_equal 1, maker.lock_version
|
|
188
201
|
end
|
|
189
202
|
end
|
|
@@ -205,14 +218,66 @@ def should_support_basic_on_duplicate_key_update
|
|
|
205
218
|
Bike::Maker.import(updated_values, on_duplicate_key_update: [:name])
|
|
206
219
|
assert Bike::Maker.count == updated_values.length
|
|
207
220
|
Bike::Maker.all.each_with_index do |maker, i|
|
|
208
|
-
assert_equal maker.name, makers[i].name
|
|
221
|
+
assert_equal maker.name, "#{makers[i].name} bikes"
|
|
209
222
|
assert_equal 1, maker.lock_version
|
|
210
223
|
end
|
|
211
224
|
end
|
|
212
225
|
end
|
|
226
|
+
|
|
227
|
+
context 'with locking disabled' do
|
|
228
|
+
it 'does not update the lock_version' do
|
|
229
|
+
users = [
|
|
230
|
+
User.new(name: 'Salomon'),
|
|
231
|
+
User.new(name: 'Nathan')
|
|
232
|
+
]
|
|
233
|
+
User.import(users)
|
|
234
|
+
assert User.count == users.length
|
|
235
|
+
User.all.each do |user|
|
|
236
|
+
assert_equal 0, user.lock_version
|
|
237
|
+
end
|
|
238
|
+
updated_users = User.all.map do |user|
|
|
239
|
+
user.name += ' Rothschild'
|
|
240
|
+
user
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
ActiveRecord::Base.lock_optimistically = false # Disable locking
|
|
244
|
+
User.import(updated_users, on_duplicate_key_update: [:name])
|
|
245
|
+
ActiveRecord::Base.lock_optimistically = true # Enable locking
|
|
246
|
+
|
|
247
|
+
assert User.count == updated_users.length
|
|
248
|
+
User.all.each_with_index do |user, i|
|
|
249
|
+
assert_equal user.name, "#{users[i].name} Rothschild"
|
|
250
|
+
assert_equal 0, user.lock_version
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
213
254
|
end
|
|
214
255
|
|
|
215
256
|
context "with :on_duplicate_key_update" do
|
|
257
|
+
describe 'using :all' do
|
|
258
|
+
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
259
|
+
let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
|
|
260
|
+
|
|
261
|
+
macro(:perform_import) do |*opts|
|
|
262
|
+
Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: :all, validate: false)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
setup do
|
|
266
|
+
values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
|
|
267
|
+
Topic.import columns + ['replies_count'], values, validate: false
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it 'updates all specified columns' do
|
|
271
|
+
perform_import
|
|
272
|
+
updated_topic = Topic.find(99)
|
|
273
|
+
assert_equal 'Book - 2nd Edition', updated_topic.title
|
|
274
|
+
assert_equal 'Jane Doe', updated_topic.author_name
|
|
275
|
+
assert_equal 'janedoe@example.com', updated_topic.author_email_address
|
|
276
|
+
assert_equal 57, updated_topic.parent_id
|
|
277
|
+
assert_equal 3, updated_topic.replies_count
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
216
281
|
describe "argument safety" do
|
|
217
282
|
it "should not modify the passed in :on_duplicate_key_update array" do
|
|
218
283
|
assert_nothing_raised do
|
|
@@ -222,6 +287,26 @@ def should_support_basic_on_duplicate_key_update
|
|
|
222
287
|
end
|
|
223
288
|
end
|
|
224
289
|
|
|
290
|
+
context "with timestamps enabled" do
|
|
291
|
+
let(:time) { Chronic.parse("5 minutes from now") }
|
|
292
|
+
|
|
293
|
+
it 'should not overwrite changed updated_at with current timestamp' do
|
|
294
|
+
topic = Topic.create(author_name: "Jane Doe", title: "Book")
|
|
295
|
+
timestamp = Time.now.utc
|
|
296
|
+
topic.updated_at = timestamp
|
|
297
|
+
Topic.import [topic], on_duplicate_key_update: :all, validate: false
|
|
298
|
+
assert_equal timestamp.to_s, Topic.last.updated_at.to_s
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'should update updated_at with current timestamp' do
|
|
302
|
+
topic = Topic.create(author_name: "Jane Doe", title: "Book")
|
|
303
|
+
Timecop.freeze(time) do
|
|
304
|
+
Topic.import [topic], on_duplicate_key_update: [:updated_at], validate: false
|
|
305
|
+
assert_in_delta time.to_i, topic.reload.updated_at.to_i, 1.second
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
225
310
|
context "with validation checks turned off" do
|
|
226
311
|
asssertion_group(:should_support_on_duplicate_key_update) do
|
|
227
312
|
should_not_update_fields_not_mentioned
|
|
@@ -260,6 +345,34 @@ def should_support_basic_on_duplicate_key_update
|
|
|
260
345
|
should_support_on_duplicate_key_update
|
|
261
346
|
should_update_fields_mentioned
|
|
262
347
|
end
|
|
348
|
+
|
|
349
|
+
context "using column aliases" do
|
|
350
|
+
let(:columns) { %w( id title author_name author_email_address parent_id ) }
|
|
351
|
+
let(:update_columns) { %w(title author_email_address parent_id) }
|
|
352
|
+
|
|
353
|
+
context "with column aliases in column list" do
|
|
354
|
+
let(:columns) { %w( id name author_name author_email_address parent_id ) }
|
|
355
|
+
should_support_on_duplicate_key_update
|
|
356
|
+
should_update_fields_mentioned
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
context "with column aliases in update columns list" do
|
|
360
|
+
let(:update_columns) { %w(name author_email_address parent_id) }
|
|
361
|
+
should_support_on_duplicate_key_update
|
|
362
|
+
should_update_fields_mentioned
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
if ENV['AR_VERSION'].to_i >= 6.0
|
|
367
|
+
context "using ignored columns" do
|
|
368
|
+
let(:columns) { %w( id title author_name author_email_address parent_id priority ) }
|
|
369
|
+
let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17, 1]] }
|
|
370
|
+
let(:update_columns) { %w(name author_email_address parent_id priority) }
|
|
371
|
+
let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57, 2]] }
|
|
372
|
+
should_support_on_duplicate_key_update
|
|
373
|
+
should_update_fields_mentioned
|
|
374
|
+
end
|
|
375
|
+
end
|
|
263
376
|
end
|
|
264
377
|
|
|
265
378
|
context "with a table that has a non-standard primary key" do
|
|
@@ -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
|
|
@@ -138,6 +140,15 @@ def should_support_recursive_import
|
|
|
138
140
|
books.each do |book|
|
|
139
141
|
assert_equal book.topic_id, second_new_topic.id
|
|
140
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_nil book.topic_id, nil
|
|
151
|
+
end
|
|
141
152
|
end
|
|
142
153
|
|
|
143
154
|
unless ENV["SKIP_COMPOSITE_PK"]
|
|
@@ -154,6 +165,24 @@ def should_support_recursive_import
|
|
|
154
165
|
assert_equal 1, tags[0].tag_id
|
|
155
166
|
assert_equal 2, tags[1].tag_id
|
|
156
167
|
end
|
|
168
|
+
|
|
169
|
+
if ENV['AR_VERSION'].to_f >= 7.1
|
|
170
|
+
it "should import models with auto-incrementing ID successfully with recursive set to true" do
|
|
171
|
+
author = Author.create!(name: "Foo Barson")
|
|
172
|
+
books = []
|
|
173
|
+
2.times do |i|
|
|
174
|
+
books << CompositeBook.new(author_id: author.id, title: "Book #{i}", composite_chapters: [
|
|
175
|
+
CompositeChapter.new(title: "Book #{i} composite chapter 1"),
|
|
176
|
+
CompositeChapter.new(title: "Book #{i} composite chapter 2"),
|
|
177
|
+
])
|
|
178
|
+
end
|
|
179
|
+
assert_difference "CompositeBook.count", +2 do
|
|
180
|
+
assert_difference "CompositeChapter.count", +4 do
|
|
181
|
+
CompositeBook.import books, recursive: true
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
157
186
|
end
|
|
158
187
|
end
|
|
159
188
|
|
|
@@ -167,7 +196,7 @@ def should_support_recursive_import
|
|
|
167
196
|
end
|
|
168
197
|
end
|
|
169
198
|
|
|
170
|
-
# If adapter supports on_duplicate_key_update, it is only applied to top level models so that SQL with invalid
|
|
199
|
+
# 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
|
|
171
200
|
# columns, keys, etc isn't generated for child associations when doing recursive import
|
|
172
201
|
if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
|
|
173
202
|
describe "on_duplicate_key_update" do
|
|
@@ -181,6 +210,103 @@ def should_support_recursive_import
|
|
|
181
210
|
end
|
|
182
211
|
end
|
|
183
212
|
end
|
|
213
|
+
|
|
214
|
+
context "when :all fields are updated" do
|
|
215
|
+
setup do
|
|
216
|
+
Topic.import new_topics, recursive: true
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "updates associated objects" do
|
|
220
|
+
new_author_name = 'Richard Bachman'
|
|
221
|
+
topic = new_topics.first
|
|
222
|
+
topic.books.each do |book|
|
|
223
|
+
book.author_name = new_author_name
|
|
224
|
+
end
|
|
225
|
+
assert_nothing_raised do
|
|
226
|
+
Topic.import new_topics, recursive: true, on_duplicate_key_update: :all
|
|
227
|
+
end
|
|
228
|
+
Topic.find(topic.id).books.each do |book|
|
|
229
|
+
assert_equal new_author_name, book.author_name
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
describe "recursive_on_duplicate_key_update" do
|
|
236
|
+
let(:new_topics) { Build(1, :topic_with_book) }
|
|
237
|
+
|
|
238
|
+
setup do
|
|
239
|
+
Topic.import new_topics, recursive: true
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "updates associated objects" do
|
|
243
|
+
new_author_name = 'Richard Bachman'
|
|
244
|
+
topic = new_topics.first
|
|
245
|
+
topic.books.each do |book|
|
|
246
|
+
book.author_name = new_author_name
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
assert_nothing_raised do
|
|
250
|
+
Topic.import new_topics,
|
|
251
|
+
recursive: true,
|
|
252
|
+
on_duplicate_key_update: [:id],
|
|
253
|
+
recursive_on_duplicate_key_update: {
|
|
254
|
+
books: { conflict_target: [:id], columns: [:author_name] }
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
Topic.find(topic.id).books.each do |book|
|
|
258
|
+
assert_equal new_author_name, book.author_name
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "updates nested associated objects" do
|
|
263
|
+
new_chapter_title = 'The Final Chapter'
|
|
264
|
+
book = new_topics.first.books.first
|
|
265
|
+
book.author_name = 'Richard Bachman'
|
|
266
|
+
|
|
267
|
+
example_chapter = book.chapters.first
|
|
268
|
+
example_chapter.title = new_chapter_title
|
|
269
|
+
|
|
270
|
+
assert_nothing_raised do
|
|
271
|
+
Topic.import new_topics,
|
|
272
|
+
recursive: true,
|
|
273
|
+
on_duplicate_key_update: [:id],
|
|
274
|
+
recursive_on_duplicate_key_update: {
|
|
275
|
+
books: { conflict_target: [:id], columns: [:author_name] },
|
|
276
|
+
chapters: { conflict_target: [:id], columns: [:title] }
|
|
277
|
+
}
|
|
278
|
+
end
|
|
279
|
+
assert_equal new_chapter_title, Chapter.find(example_chapter.id).title
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# If returning option is provided, it is only applied to top level models so that SQL with invalid
|
|
285
|
+
# columns, keys, etc isn't generated for child associations when doing recursive import
|
|
286
|
+
describe "returning" do
|
|
287
|
+
let(:new_topics) { Build(1, :topic_with_book) }
|
|
288
|
+
|
|
289
|
+
it "imports objects with associations" do
|
|
290
|
+
assert_difference "Topic.count", +1 do
|
|
291
|
+
Topic.import new_topics, recursive: true, returning: [:content], validate: false
|
|
292
|
+
new_topics.each do |topic|
|
|
293
|
+
assert_not_nil topic.id
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# If no returning option is provided, it is ignored
|
|
300
|
+
describe "no returning" do
|
|
301
|
+
let(:new_topics) { Build(1, :topic_with_book) }
|
|
302
|
+
|
|
303
|
+
it "is ignored and imports objects with associations" do
|
|
304
|
+
assert_difference "Topic.count", +1 do
|
|
305
|
+
Topic.import new_topics, recursive: true, no_returning: true, validate: false
|
|
306
|
+
new_topics.each do |topic|
|
|
307
|
+
assert_not_nil topic.id
|
|
308
|
+
end
|
|
309
|
+
end
|
|
184
310
|
end
|
|
185
311
|
end
|
|
186
312
|
end
|