activerecord-import 0.10.0 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +49 -0
  4. data/.rubocop_todo.yml +36 -0
  5. data/.travis.yml +64 -8
  6. data/CHANGELOG.md +475 -0
  7. data/Gemfile +32 -15
  8. data/LICENSE +21 -56
  9. data/README.markdown +564 -35
  10. data/Rakefile +20 -3
  11. data/activerecord-import.gemspec +7 -7
  12. data/benchmarks/README +2 -2
  13. data/benchmarks/benchmark.rb +68 -64
  14. data/benchmarks/lib/base.rb +138 -137
  15. data/benchmarks/lib/cli_parser.rb +107 -103
  16. data/benchmarks/lib/{mysql_benchmark.rb → mysql2_benchmark.rb} +19 -22
  17. data/benchmarks/lib/output_to_csv.rb +5 -4
  18. data/benchmarks/lib/output_to_html.rb +8 -13
  19. data/benchmarks/models/test_innodb.rb +1 -1
  20. data/benchmarks/models/test_memory.rb +1 -1
  21. data/benchmarks/models/test_myisam.rb +1 -1
  22. data/benchmarks/schema/mysql2_schema.rb +16 -0
  23. data/gemfiles/3.2.gemfile +2 -4
  24. data/gemfiles/4.0.gemfile +2 -4
  25. data/gemfiles/4.1.gemfile +2 -4
  26. data/gemfiles/4.2.gemfile +2 -4
  27. data/gemfiles/5.0.gemfile +2 -0
  28. data/gemfiles/5.1.gemfile +2 -0
  29. data/gemfiles/5.2.gemfile +2 -0
  30. data/gemfiles/6.0.gemfile +2 -0
  31. data/gemfiles/6.1.gemfile +1 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +6 -0
  33. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +0 -1
  34. data/lib/activerecord-import/adapters/abstract_adapter.rb +23 -17
  35. data/lib/activerecord-import/adapters/mysql_adapter.rb +52 -25
  36. data/lib/activerecord-import/adapters/postgresql_adapter.rb +187 -10
  37. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +148 -17
  38. data/lib/activerecord-import/base.rb +15 -9
  39. data/lib/activerecord-import/import.rb +740 -191
  40. data/lib/activerecord-import/synchronize.rb +21 -21
  41. data/lib/activerecord-import/value_sets_parser.rb +33 -8
  42. data/lib/activerecord-import/version.rb +1 -1
  43. data/lib/activerecord-import.rb +4 -15
  44. data/test/adapters/jdbcsqlite3.rb +1 -0
  45. data/test/adapters/makara_postgis.rb +1 -0
  46. data/test/adapters/mysql2_makara.rb +1 -0
  47. data/test/adapters/mysql2spatial.rb +1 -1
  48. data/test/adapters/postgis.rb +1 -1
  49. data/test/adapters/postgresql.rb +1 -1
  50. data/test/adapters/postgresql_makara.rb +1 -0
  51. data/test/adapters/spatialite.rb +1 -1
  52. data/test/adapters/sqlite3.rb +1 -1
  53. data/test/database.yml.sample +13 -18
  54. data/test/import_test.rb +608 -89
  55. data/test/jdbcmysql/import_test.rb +2 -3
  56. data/test/jdbcpostgresql/import_test.rb +0 -2
  57. data/test/jdbcsqlite3/import_test.rb +4 -0
  58. data/test/makara_postgis/import_test.rb +8 -0
  59. data/test/models/account.rb +3 -0
  60. data/test/models/alarm.rb +2 -0
  61. data/test/models/animal.rb +6 -0
  62. data/test/models/bike_maker.rb +7 -0
  63. data/test/models/book.rb +7 -6
  64. data/test/models/car.rb +3 -0
  65. data/test/models/chapter.rb +2 -2
  66. data/test/models/dictionary.rb +4 -0
  67. data/test/models/discount.rb +3 -0
  68. data/test/models/end_note.rb +2 -2
  69. data/test/models/promotion.rb +3 -0
  70. data/test/models/question.rb +3 -0
  71. data/test/models/rule.rb +3 -0
  72. data/test/models/tag.rb +4 -0
  73. data/test/models/topic.rb +17 -3
  74. data/test/models/user.rb +3 -0
  75. data/test/models/user_token.rb +4 -0
  76. data/test/models/vendor.rb +7 -0
  77. data/test/models/widget.rb +19 -2
  78. data/test/mysql2/import_test.rb +2 -3
  79. data/test/{em_mysql2 → mysql2_makara}/import_test.rb +1 -1
  80. data/test/mysqlspatial2/import_test.rb +2 -2
  81. data/test/postgis/import_test.rb +5 -1
  82. data/test/schema/generic_schema.rb +159 -85
  83. data/test/schema/jdbcpostgresql_schema.rb +1 -0
  84. data/test/schema/mysql2_schema.rb +19 -0
  85. data/test/schema/postgis_schema.rb +1 -0
  86. data/test/schema/postgresql_schema.rb +61 -0
  87. data/test/schema/sqlite3_schema.rb +13 -0
  88. data/test/sqlite3/import_test.rb +2 -50
  89. data/test/support/active_support/test_case_extensions.rb +21 -13
  90. data/test/support/{mysql/assertions.rb → assertions.rb} +20 -2
  91. data/test/support/factories.rb +39 -14
  92. data/test/support/generate.rb +10 -10
  93. data/test/support/mysql/import_examples.rb +49 -98
  94. data/test/support/postgresql/import_examples.rb +535 -57
  95. data/test/support/shared_examples/on_duplicate_key_ignore.rb +43 -0
  96. data/test/support/shared_examples/on_duplicate_key_update.rb +378 -0
  97. data/test/support/shared_examples/recursive_import.rb +225 -0
  98. data/test/support/sqlite3/import_examples.rb +231 -0
  99. data/test/synchronize_test.rb +10 -2
  100. data/test/test_helper.rb +36 -8
  101. data/test/travis/database.yml +26 -17
  102. data/test/value_sets_bytes_parser_test.rb +25 -17
  103. data/test/value_sets_records_parser_test.rb +6 -6
  104. metadata +86 -42
  105. data/benchmarks/boot.rb +0 -18
  106. data/benchmarks/schema/mysql_schema.rb +0 -16
  107. data/gemfiles/3.1.gemfile +0 -4
  108. data/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb +0 -8
  109. data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +0 -6
  110. data/lib/activerecord-import/em_mysql2.rb +0 -7
  111. data/lib/activerecord-import/mysql.rb +0 -7
  112. data/test/adapters/em_mysql2.rb +0 -1
  113. data/test/adapters/mysql.rb +0 -1
  114. data/test/adapters/mysqlspatial.rb +0 -1
  115. data/test/mysql/import_test.rb +0 -6
  116. data/test/mysqlspatial/import_test.rb +0 -6
  117. data/test/schema/mysql_schema.rb +0 -18
  118. data/test/travis/build.sh +0 -30
@@ -0,0 +1,378 @@
1
+ def should_support_basic_on_duplicate_key_update
2
+ describe "#import" do
3
+ extend ActiveSupport::TestCase::ImportAssertions
4
+
5
+ macro(:perform_import) { raise "supply your own #perform_import in a context below" }
6
+ macro(:updated_topic) { Topic.find(@topic.id) }
7
+
8
+ context "with lock_version upsert" do
9
+ describe 'optimistic lock' do
10
+ it 'lock_version upsert after on_duplcate_key_update by model' do
11
+ users = [
12
+ User.new(name: 'Salomon'),
13
+ User.new(name: 'Nathan')
14
+ ]
15
+ User.import(users)
16
+ assert User.count == users.length
17
+ User.all.each do |user|
18
+ assert_equal 0, user.lock_version
19
+ end
20
+ updated_users = User.all.map do |user|
21
+ user.name += ' Rothschild'
22
+ user
23
+ end
24
+ User.import(updated_users, on_duplicate_key_update: [:name])
25
+ assert User.count == updated_users.length
26
+ User.all.each_with_index do |user, i|
27
+ assert_equal user.name, users[i].name + ' Rothschild'
28
+ assert_equal 1, user.lock_version
29
+ end
30
+ end
31
+
32
+ it 'lock_version upsert after on_duplcate_key_update by array' do
33
+ users = [
34
+ User.new(name: 'Salomon'),
35
+ User.new(name: 'Nathan')
36
+ ]
37
+ User.import(users)
38
+ assert User.count == users.length
39
+ User.all.each do |user|
40
+ assert_equal 0, user.lock_version
41
+ end
42
+
43
+ columns = [:id, :name]
44
+ updated_values = User.all.map do |user|
45
+ user.name += ' Rothschild'
46
+ [user.id, user.name]
47
+ end
48
+ User.import(columns, updated_values, on_duplicate_key_update: [:name])
49
+ assert User.count == updated_values.length
50
+ User.all.each_with_index do |user, i|
51
+ assert_equal user.name, users[i].name + ' Rothschild'
52
+ assert_equal 1, user.lock_version
53
+ end
54
+ end
55
+
56
+ it 'lock_version upsert after on_duplcate_key_update by hash' do
57
+ users = [
58
+ User.new(name: 'Salomon'),
59
+ User.new(name: 'Nathan')
60
+ ]
61
+ User.import(users)
62
+ assert User.count == users.length
63
+ User.all.each do |user|
64
+ assert_equal 0, user.lock_version
65
+ end
66
+ updated_values = User.all.map do |user|
67
+ user.name += ' Rothschild'
68
+ { id: user.id, name: user.name }
69
+ end
70
+ User.import(updated_values, on_duplicate_key_update: [:name])
71
+ assert User.count == updated_values.length
72
+ User.all.each_with_index do |user, i|
73
+ assert_equal user.name, users[i].name + ' Rothschild'
74
+ assert_equal 1, user.lock_version
75
+ end
76
+ updated_values2 = User.all.map do |user|
77
+ user.name += ' jr.'
78
+ { id: user.id, name: user.name }
79
+ end
80
+ User.import(updated_values2, on_duplicate_key_update: [:name])
81
+ assert User.count == updated_values2.length
82
+ User.all.each_with_index do |user, i|
83
+ assert_equal user.name, users[i].name + ' Rothschild jr.'
84
+ assert_equal 2, user.lock_version
85
+ end
86
+ end
87
+
88
+ it 'upsert optimistic lock columns other than lock_version by model' do
89
+ accounts = [
90
+ Account.new(name: 'Salomon'),
91
+ Account.new(name: 'Nathan')
92
+ ]
93
+ Account.import(accounts)
94
+ assert Account.count == accounts.length
95
+ Account.all.each do |user|
96
+ assert_equal 0, user.lock
97
+ end
98
+ updated_accounts = Account.all.map do |user|
99
+ user.name += ' Rothschild'
100
+ user
101
+ end
102
+ Account.import(updated_accounts, on_duplicate_key_update: [:id, :name])
103
+ assert Account.count == updated_accounts.length
104
+ Account.all.each_with_index do |user, i|
105
+ assert_equal user.name, accounts[i].name + ' Rothschild'
106
+ assert_equal 1, user.lock
107
+ end
108
+ end
109
+
110
+ it 'upsert optimistic lock columns other than lock_version by array' do
111
+ accounts = [
112
+ Account.new(name: 'Salomon'),
113
+ Account.new(name: 'Nathan')
114
+ ]
115
+ Account.import(accounts)
116
+ assert Account.count == accounts.length
117
+ Account.all.each do |user|
118
+ assert_equal 0, user.lock
119
+ end
120
+
121
+ columns = [:id, :name]
122
+ updated_values = Account.all.map do |user|
123
+ user.name += ' Rothschild'
124
+ [user.id, user.name]
125
+ end
126
+ Account.import(columns, updated_values, on_duplicate_key_update: [:name])
127
+ assert Account.count == updated_values.length
128
+ Account.all.each_with_index do |user, i|
129
+ assert_equal user.name, accounts[i].name + ' Rothschild'
130
+ assert_equal 1, user.lock
131
+ end
132
+ end
133
+
134
+ it 'upsert optimistic lock columns other than lock_version by hash' do
135
+ accounts = [
136
+ Account.new(name: 'Salomon'),
137
+ Account.new(name: 'Nathan')
138
+ ]
139
+ Account.import(accounts)
140
+ assert Account.count == accounts.length
141
+ Account.all.each do |user|
142
+ assert_equal 0, user.lock
143
+ end
144
+ updated_values = Account.all.map do |user|
145
+ user.name += ' Rothschild'
146
+ { id: user.id, name: user.name }
147
+ end
148
+ Account.import(updated_values, on_duplicate_key_update: [:name])
149
+ assert Account.count == updated_values.length
150
+ Account.all.each_with_index do |user, i|
151
+ assert_equal user.name, accounts[i].name + ' Rothschild'
152
+ assert_equal 1, user.lock
153
+ end
154
+ end
155
+
156
+ it 'update the lock_version of models separated by namespaces by model' do
157
+ makers = [
158
+ Bike::Maker.new(name: 'Yamaha'),
159
+ Bike::Maker.new(name: 'Honda')
160
+ ]
161
+ Bike::Maker.import(makers)
162
+ assert Bike::Maker.count == makers.length
163
+ Bike::Maker.all.each do |maker|
164
+ assert_equal 0, maker.lock_version
165
+ end
166
+ updated_makers = Bike::Maker.all.map do |maker|
167
+ maker.name += ' bikes'
168
+ maker
169
+ end
170
+ Bike::Maker.import(updated_makers, on_duplicate_key_update: [:name])
171
+ assert Bike::Maker.count == updated_makers.length
172
+ Bike::Maker.all.each_with_index do |maker, i|
173
+ assert_equal maker.name, makers[i].name + ' bikes'
174
+ assert_equal 1, maker.lock_version
175
+ end
176
+ end
177
+ it 'update the lock_version of models separated by namespaces by array' do
178
+ makers = [
179
+ Bike::Maker.new(name: 'Yamaha'),
180
+ Bike::Maker.new(name: 'Honda')
181
+ ]
182
+ Bike::Maker.import(makers)
183
+ assert Bike::Maker.count == makers.length
184
+ Bike::Maker.all.each do |maker|
185
+ assert_equal 0, maker.lock_version
186
+ end
187
+
188
+ columns = [:id, :name]
189
+ updated_values = Bike::Maker.all.map do |maker|
190
+ maker.name += ' bikes'
191
+ [maker.id, maker.name]
192
+ end
193
+ Bike::Maker.import(columns, updated_values, on_duplicate_key_update: [:name])
194
+ assert Bike::Maker.count == updated_values.length
195
+ Bike::Maker.all.each_with_index do |maker, i|
196
+ assert_equal maker.name, makers[i].name + ' bikes'
197
+ assert_equal 1, maker.lock_version
198
+ end
199
+ end
200
+
201
+ it 'update the lock_version of models separated by namespaces by hash' do
202
+ makers = [
203
+ Bike::Maker.new(name: 'Yamaha'),
204
+ Bike::Maker.new(name: 'Honda')
205
+ ]
206
+ Bike::Maker.import(makers)
207
+ assert Bike::Maker.count == makers.length
208
+ Bike::Maker.all.each do |maker|
209
+ assert_equal 0, maker.lock_version
210
+ end
211
+ updated_values = Bike::Maker.all.map do |maker|
212
+ maker.name += ' bikes'
213
+ { id: maker.id, name: maker.name }
214
+ end
215
+ Bike::Maker.import(updated_values, on_duplicate_key_update: [:name])
216
+ assert Bike::Maker.count == updated_values.length
217
+ Bike::Maker.all.each_with_index do |maker, i|
218
+ assert_equal maker.name, makers[i].name + ' bikes'
219
+ assert_equal 1, maker.lock_version
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ context "with :on_duplicate_key_update" do
226
+ describe 'using :all' do
227
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
228
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
229
+
230
+ macro(:perform_import) do |*opts|
231
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: :all, validate: false)
232
+ end
233
+
234
+ setup do
235
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
236
+ Topic.import columns + ['replies_count'], values, validate: false
237
+ end
238
+
239
+ it 'updates all specified columns' do
240
+ perform_import
241
+ updated_topic = Topic.find(99)
242
+ assert_equal 'Book - 2nd Edition', updated_topic.title
243
+ assert_equal 'Jane Doe', updated_topic.author_name
244
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
245
+ assert_equal 57, updated_topic.parent_id
246
+ assert_equal 3, updated_topic.replies_count
247
+ end
248
+ end
249
+
250
+ describe "argument safety" do
251
+ it "should not modify the passed in :on_duplicate_key_update array" do
252
+ assert_nothing_raised do
253
+ columns = %w(title author_name).freeze
254
+ Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: columns
255
+ end
256
+ end
257
+ end
258
+
259
+ context "with timestamps enabled" do
260
+ let(:time) { Chronic.parse("5 minutes from now") }
261
+
262
+ it 'should not overwrite changed updated_at with current timestamp' do
263
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
264
+ timestamp = Time.now.utc
265
+ topic.updated_at = timestamp
266
+ Topic.import [topic], on_duplicate_key_update: :all, validate: false
267
+ assert_equal timestamp.to_s, Topic.last.updated_at.to_s
268
+ end
269
+
270
+ it 'should update updated_at with current timestamp' do
271
+ topic = Topic.create(author_name: "Jane Doe", title: "Book")
272
+ Timecop.freeze(time) do
273
+ Topic.import [topic], on_duplicate_key_update: [:updated_at], validate: false
274
+ assert_in_delta time.to_i, topic.reload.updated_at.to_i, 1.second
275
+ end
276
+ end
277
+ end
278
+
279
+ context "with validation checks turned off" do
280
+ asssertion_group(:should_support_on_duplicate_key_update) do
281
+ should_not_update_fields_not_mentioned
282
+ should_update_foreign_keys
283
+ should_not_update_created_at_on_timestamp_columns
284
+ should_update_updated_at_on_timestamp_columns
285
+ end
286
+
287
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
288
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
289
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
290
+
291
+ macro(:perform_import) do |*opts|
292
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: update_columns, validate: false)
293
+ end
294
+
295
+ setup do
296
+ Topic.import columns, values, validate: false
297
+ @topic = Topic.find 99
298
+ end
299
+
300
+ context "using an empty array" do
301
+ let(:update_columns) { [] }
302
+ should_not_update_fields_not_mentioned
303
+ should_update_updated_at_on_timestamp_columns
304
+ end
305
+
306
+ context "using string column names" do
307
+ let(:update_columns) { %w(title author_email_address parent_id) }
308
+ should_support_on_duplicate_key_update
309
+ should_update_fields_mentioned
310
+ end
311
+
312
+ context "using symbol column names" do
313
+ let(:update_columns) { [:title, :author_email_address, :parent_id] }
314
+ should_support_on_duplicate_key_update
315
+ should_update_fields_mentioned
316
+ end
317
+ end
318
+
319
+ context "with a table that has a non-standard primary key" do
320
+ let(:columns) { [:promotion_id, :code] }
321
+ let(:values) { [[1, 'DISCOUNT1']] }
322
+ let(:updated_values) { [[1, 'DISCOUNT2']] }
323
+ let(:update_columns) { [:code] }
324
+
325
+ macro(:perform_import) do |*opts|
326
+ Promotion.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: update_columns, validate: false)
327
+ end
328
+ macro(:updated_promotion) { Promotion.find(@promotion.promotion_id) }
329
+
330
+ setup do
331
+ Promotion.import columns, values, validate: false
332
+ @promotion = Promotion.find 1
333
+ end
334
+
335
+ it "should update specified columns" do
336
+ perform_import
337
+ assert_equal 'DISCOUNT2', updated_promotion.code
338
+ end
339
+ end
340
+
341
+ unless ENV["SKIP_COMPOSITE_PK"]
342
+ context "with composite primary keys" do
343
+ it "should import array of values successfully" do
344
+ columns = [:tag_id, :publisher_id, :tag]
345
+ Tag.import columns, [[1, 1, 'Mystery']], validate: false
346
+
347
+ assert_difference "Tag.count", +0 do
348
+ Tag.import columns, [[1, 1, 'Science']], on_duplicate_key_update: [:tag], validate: false
349
+ end
350
+ assert_equal 'Science', Tag.first.tag
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ context "with :on_duplicate_key_update turned off" do
357
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
358
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }
359
+ let(:updated_values) { [[100, "Book - 2nd Edition", "This should raise an exception", "john@nogo.com", 57]] }
360
+
361
+ macro(:perform_import) do |*opts|
362
+ # `on_duplicate_key_update: false` is the tested feature
363
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: false, validate: false)
364
+ end
365
+
366
+ setup do
367
+ Topic.import columns, values, validate: false
368
+ @topic = Topic.find 100
369
+ end
370
+
371
+ it "should raise ActiveRecord::RecordNotUnique" do
372
+ assert_raise ActiveRecord::RecordNotUnique do
373
+ perform_import
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
@@ -0,0 +1,225 @@
1
+ def should_support_recursive_import
2
+ describe "importing objects with associations" do
3
+ let(:new_topics) { Build(num_topics, :topic_with_book) }
4
+ let(:new_topics_with_invalid_chapter) do
5
+ chapter = new_topics.first.books.first.chapters.first
6
+ chapter.title = nil
7
+ new_topics
8
+ end
9
+ let(:num_topics) { 3 }
10
+ let(:num_books) { 6 }
11
+ let(:num_chapters) { 18 }
12
+ let(:num_endnotes) { 24 }
13
+
14
+ let(:new_question_with_rule) { FactoryBot.build :question, :with_rule }
15
+
16
+ it 'imports top level' do
17
+ assert_difference "Topic.count", +num_topics do
18
+ Topic.import new_topics, recursive: true
19
+ new_topics.each do |topic|
20
+ assert_not_nil topic.id
21
+ end
22
+ end
23
+ end
24
+
25
+ it 'imports first level associations' do
26
+ assert_difference "Book.count", +num_books do
27
+ Topic.import new_topics, recursive: true
28
+ new_topics.each do |topic|
29
+ topic.books.each do |book|
30
+ assert_equal topic.id, book.topic_id
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ it 'imports polymorphic associations' do
37
+ discounts = Array.new(1) { |i| Discount.new(amount: i) }
38
+ books = Array.new(1) { |i| Book.new(author_name: "Author ##{i}", title: "Book ##{i}") }
39
+ books.each do |book|
40
+ book.discounts << discounts
41
+ end
42
+ Book.import books, recursive: true
43
+ books.each do |book|
44
+ book.discounts.each do |discount|
45
+ assert_not_nil discount.discountable_id
46
+ assert_equal 'Book', discount.discountable_type
47
+ end
48
+ end
49
+ end
50
+
51
+ it 'imports polymorphic associations from subclass' do
52
+ discounts = Array.new(1) { |i| Discount.new(amount: i) }
53
+ dictionaries = Array.new(1) { |i| Dictionary.new(author_name: "Author ##{i}", title: "Book ##{i}") }
54
+ dictionaries.each do |dictionary|
55
+ dictionary.discounts << discounts
56
+ end
57
+ Dictionary.import dictionaries, recursive: true
58
+ assert_equal 1, Dictionary.last.discounts.count
59
+ dictionaries.each do |dictionary|
60
+ dictionary.discounts.each do |discount|
61
+ assert_not_nil discount.discountable_id
62
+ assert_equal 'Book', discount.discountable_type
63
+ end
64
+ end
65
+ end
66
+
67
+ [{ recursive: false }, {}].each do |import_options|
68
+ it "skips recursion for #{import_options}" do
69
+ assert_difference "Book.count", 0 do
70
+ Topic.import new_topics, import_options
71
+ end
72
+ end
73
+ end
74
+
75
+ it 'imports deeper nested associations' do
76
+ assert_difference "Chapter.count", +num_chapters do
77
+ assert_difference "EndNote.count", +num_endnotes do
78
+ Topic.import new_topics, recursive: true
79
+ new_topics.each do |topic|
80
+ topic.books.each do |book|
81
+ book.chapters.each do |chapter|
82
+ assert_equal book.id, chapter.book_id
83
+ end
84
+ book.end_notes.each do |endnote|
85
+ assert_equal book.id, endnote.book_id
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ # Models are only valid if all associations are valid
94
+ it "only imports models with valid associations" do
95
+ assert_difference "Topic.count", 2 do
96
+ assert_difference "Book.count", 4 do
97
+ assert_difference "Chapter.count", 12 do
98
+ assert_difference "EndNote.count", 16 do
99
+ Topic.import new_topics_with_invalid_chapter, recursive: true
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ it "skips validation of the associations if requested" do
107
+ assert_difference "Chapter.count", +num_chapters do
108
+ Topic.import new_topics_with_invalid_chapter, validate: false, recursive: true
109
+ end
110
+ end
111
+
112
+ it 'imports has_one associations' do
113
+ assert_difference 'Rule.count' do
114
+ Question.import [new_question_with_rule], recursive: true
115
+ end
116
+ end
117
+
118
+ it "imports an imported belongs_to association id" do
119
+ first_new_topic = new_topics[0]
120
+ second_new_topic = new_topics[1]
121
+
122
+ books = first_new_topic.books.to_a
123
+ Topic.import new_topics, validate: false
124
+
125
+ assert_difference "Book.count", books.size do
126
+ Book.import books, validate: false
127
+ end
128
+
129
+ books.each do |book|
130
+ assert_equal book.topic_id, first_new_topic.id
131
+ end
132
+
133
+ books.each { |book| book.topic_id = second_new_topic.id }
134
+ assert_no_difference "Book.count", books.size do
135
+ Book.import books, validate: false, on_duplicate_key_update: [:topic_id]
136
+ end
137
+
138
+ books.each do |book|
139
+ assert_equal book.topic_id, second_new_topic.id
140
+ end
141
+
142
+ books.each { |book| book.topic_id = nil }
143
+ assert_no_difference "Book.count", books.size do
144
+ Book.import books, validate: false, on_duplicate_key_update: [:topic_id]
145
+ end
146
+
147
+ books.each do |book|
148
+ assert_equal book.topic_id, nil
149
+ end
150
+ end
151
+
152
+ unless ENV["SKIP_COMPOSITE_PK"]
153
+ describe "with composite primary keys" do
154
+ it "should import models and set id" do
155
+ tags = []
156
+ tags << Tag.new(tag_id: 1, publisher_id: 1, tag: 'Mystery')
157
+ tags << Tag.new(tag_id: 2, publisher_id: 1, tag: 'Science')
158
+
159
+ assert_difference "Tag.count", +2 do
160
+ Tag.import tags
161
+ end
162
+
163
+ assert_equal 1, tags[0].tag_id
164
+ assert_equal 2, tags[1].tag_id
165
+ end
166
+ end
167
+ end
168
+
169
+ describe "all_or_none" do
170
+ [Book, Chapter, Topic, EndNote].each do |type|
171
+ it "creates #{type}" do
172
+ assert_difference "#{type}.count", 0 do
173
+ Topic.import new_topics_with_invalid_chapter, all_or_none: true, recursive: true
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ # If adapter supports on_duplicate_key_update, it is only applied to top level models so that SQL with invalid
180
+ # columns, keys, etc isn't generated for child associations when doing recursive import
181
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
182
+ describe "on_duplicate_key_update" do
183
+ let(:new_topics) { Build(1, :topic_with_book) }
184
+
185
+ it "imports objects with associations" do
186
+ assert_difference "Topic.count", +1 do
187
+ Topic.import new_topics, recursive: true, on_duplicate_key_update: [:updated_at], validate: false
188
+ new_topics.each do |topic|
189
+ assert_not_nil topic.id
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ # If returning option is provided, it is only applied to top level models so that SQL with invalid
197
+ # columns, keys, etc isn't generated for child associations when doing recursive import
198
+ describe "returning" do
199
+ let(:new_topics) { Build(1, :topic_with_book) }
200
+
201
+ it "imports objects with associations" do
202
+ assert_difference "Topic.count", +1 do
203
+ Topic.import new_topics, recursive: true, returning: [:content], validate: false
204
+ new_topics.each do |topic|
205
+ assert_not_nil topic.id
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ # If no returning option is provided, it is ignored
212
+ describe "no returning" do
213
+ let(:new_topics) { Build(1, :topic_with_book) }
214
+
215
+ it "is ignored and imports objects with associations" do
216
+ assert_difference "Topic.count", +1 do
217
+ Topic.import new_topics, recursive: true, no_returning: true, validate: false
218
+ new_topics.each do |topic|
219
+ assert_not_nil topic.id
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end