activerecord-import 1.0.3

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