activerecord-import 0.10.0 → 1.0.8

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 (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