activerecord-import 0.14.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +448 -2
  7. data/Gemfile +26 -19
  8. data/LICENSE +21 -56
  9. data/README.markdown +568 -32
  10. data/Rakefile +5 -1
  11. data/activerecord-import.gemspec +8 -7
  12. data/benchmarks/README +2 -2
  13. data/benchmarks/benchmark.rb +8 -1
  14. data/benchmarks/lib/base.rb +2 -0
  15. data/benchmarks/lib/cli_parser.rb +5 -2
  16. data/benchmarks/lib/float.rb +2 -0
  17. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  18. data/benchmarks/lib/output_to_csv.rb +2 -0
  19. data/benchmarks/lib/output_to_html.rb +4 -2
  20. data/benchmarks/models/test_innodb.rb +2 -0
  21. data/benchmarks/models/test_memory.rb +2 -0
  22. data/benchmarks/models/test_myisam.rb +2 -0
  23. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  24. data/gemfiles/4.2.gemfile +4 -3
  25. data/gemfiles/5.0.gemfile +4 -3
  26. data/gemfiles/5.1.gemfile +4 -0
  27. data/gemfiles/5.2.gemfile +4 -0
  28. data/gemfiles/6.0.gemfile +4 -0
  29. data/gemfiles/6.1.gemfile +4 -0
  30. data/gemfiles/7.0.gemfile +4 -0
  31. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  33. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +8 -0
  35. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  38. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  39. data/lib/activerecord-import/adapters/abstract_adapter.rb +12 -16
  40. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  42. data/lib/activerecord-import/adapters/mysql_adapter.rb +35 -18
  43. data/lib/activerecord-import/adapters/postgresql_adapter.rb +107 -28
  44. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +141 -18
  45. data/lib/activerecord-import/base.rb +15 -8
  46. data/lib/activerecord-import/import.rb +642 -178
  47. data/lib/activerecord-import/mysql2.rb +2 -0
  48. data/lib/activerecord-import/postgresql.rb +2 -0
  49. data/lib/activerecord-import/sqlite3.rb +2 -0
  50. data/lib/activerecord-import/synchronize.rb +5 -3
  51. data/lib/activerecord-import/value_sets_parser.rb +29 -3
  52. data/lib/activerecord-import/version.rb +3 -1
  53. data/lib/activerecord-import.rb +5 -16
  54. data/test/adapters/jdbcmysql.rb +2 -0
  55. data/test/adapters/jdbcpostgresql.rb +2 -0
  56. data/test/adapters/jdbcsqlite3.rb +3 -0
  57. data/test/adapters/makara_postgis.rb +3 -0
  58. data/test/adapters/mysql2.rb +2 -0
  59. data/test/adapters/mysql2_makara.rb +2 -0
  60. data/test/adapters/mysql2spatial.rb +2 -0
  61. data/test/adapters/postgis.rb +2 -0
  62. data/test/adapters/postgresql.rb +2 -0
  63. data/test/adapters/postgresql_makara.rb +2 -0
  64. data/test/adapters/seamless_database_pool.rb +2 -0
  65. data/test/adapters/spatialite.rb +2 -0
  66. data/test/adapters/sqlite3.rb +2 -0
  67. data/test/{travis → github}/database.yml +7 -1
  68. data/test/import_test.rb +498 -32
  69. data/test/jdbcmysql/import_test.rb +2 -1
  70. data/test/jdbcpostgresql/import_test.rb +2 -1
  71. data/test/jdbcsqlite3/import_test.rb +6 -0
  72. data/test/makara_postgis/import_test.rb +10 -0
  73. data/test/models/account.rb +5 -0
  74. data/test/models/alarm.rb +4 -0
  75. data/test/models/animal.rb +8 -0
  76. data/test/models/bike_maker.rb +9 -0
  77. data/test/models/book.rb +4 -0
  78. data/test/models/car.rb +5 -0
  79. data/test/models/card.rb +5 -0
  80. data/test/models/chapter.rb +2 -0
  81. data/test/models/customer.rb +8 -0
  82. data/test/models/deck.rb +8 -0
  83. data/test/models/dictionary.rb +6 -0
  84. data/test/models/discount.rb +2 -0
  85. data/test/models/end_note.rb +2 -0
  86. data/test/models/group.rb +2 -0
  87. data/test/models/order.rb +8 -0
  88. data/test/models/playing_card.rb +4 -0
  89. data/test/models/promotion.rb +2 -0
  90. data/test/models/question.rb +2 -0
  91. data/test/models/rule.rb +2 -0
  92. data/test/models/tag.rb +7 -0
  93. data/test/models/tag_alias.rb +5 -0
  94. data/test/models/topic.rb +16 -0
  95. data/test/models/user.rb +5 -0
  96. data/test/models/user_token.rb +6 -0
  97. data/test/models/vendor.rb +9 -0
  98. data/test/models/widget.rb +18 -0
  99. data/test/mysql2/import_test.rb +2 -0
  100. data/test/mysql2_makara/import_test.rb +2 -0
  101. data/test/mysqlspatial2/import_test.rb +2 -0
  102. data/test/postgis/import_test.rb +6 -0
  103. data/test/postgresql/import_test.rb +2 -4
  104. data/test/schema/generic_schema.rb +88 -3
  105. data/test/schema/jdbcpostgresql_schema.rb +3 -0
  106. data/test/schema/mysql2_schema.rb +21 -0
  107. data/test/schema/postgis_schema.rb +3 -0
  108. data/test/schema/postgresql_schema.rb +63 -0
  109. data/test/schema/sqlite3_schema.rb +15 -0
  110. data/test/schema/version.rb +2 -0
  111. data/test/sqlite3/import_test.rb +4 -50
  112. data/test/support/active_support/test_case_extensions.rb +8 -1
  113. data/test/support/assertions.rb +2 -0
  114. data/test/support/factories.rb +17 -8
  115. data/test/support/generate.rb +10 -8
  116. data/test/support/mysql/import_examples.rb +17 -3
  117. data/test/support/postgresql/import_examples.rb +442 -9
  118. data/test/support/shared_examples/on_duplicate_key_ignore.rb +45 -0
  119. data/test/support/shared_examples/on_duplicate_key_update.rb +278 -1
  120. data/test/support/shared_examples/recursive_import.rb +137 -12
  121. data/test/support/sqlite3/import_examples.rb +232 -0
  122. data/test/synchronize_test.rb +10 -0
  123. data/test/test_helper.rb +44 -3
  124. data/test/value_sets_bytes_parser_test.rb +15 -2
  125. data/test/value_sets_records_parser_test.rb +2 -0
  126. metadata +74 -22
  127. data/.travis.yml +0 -52
  128. data/gemfiles/3.2.gemfile +0 -3
  129. data/gemfiles/4.0.gemfile +0 -3
  130. data/gemfiles/4.1.gemfile +0 -3
  131. data/test/schema/mysql_schema.rb +0 -16
data/test/import_test.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path('../test_helper', __FILE__)
2
4
 
3
5
  describe "#import" do
@@ -12,6 +14,16 @@ describe "#import" do
12
14
  end
13
15
  end
14
16
 
17
+ it "warns you that you're using the library wrong" do
18
+ error = assert_raise(ArgumentError) { Topic.import %w(title author_name), ['Author #1', 'Book #1', 0] }
19
+ assert_equal error.message, "Last argument should be a two dimensional array '[[]]'. First element in array was a String"
20
+ end
21
+
22
+ it "warns you that you're passing more data than you ought to" do
23
+ error = assert_raise(ArgumentError) { Topic.import %w(title author_name), [['Author #1', 'Book #1', 0]] }
24
+ assert_equal error.message, "Number of values (8) exceeds number of columns (7)"
25
+ end
26
+
15
27
  it "should not produce an error when importing empty arrays" do
16
28
  assert_nothing_raised do
17
29
  Topic.import []
@@ -29,7 +41,8 @@ describe "#import" do
29
41
 
30
42
  it "should not modify the passed in values array" do
31
43
  assert_nothing_raised do
32
- values = [%w(foo bar)].freeze
44
+ record = %w(foo bar).freeze
45
+ values = [record].freeze
33
46
  Topic.import %w(title author_name), values
34
47
  end
35
48
  end
@@ -42,14 +55,162 @@ describe "#import" do
42
55
  Widget.import Build(3, :widgets)
43
56
  end
44
57
  end
58
+
59
+ context "with uppercase letters" do
60
+ it "should import models successfully" do
61
+ assert_difference "Car.count", +3 do
62
+ Car.import Build(3, :cars)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ context "that have no primary key" do
69
+ it "should import models successfully" do
70
+ assert_difference "Rule.count", +3 do
71
+ Rule.import Build(3, :rules)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "with an array of hashes" do
78
+ let(:columns) { [:title, :author_name] }
79
+ let(:values) { [{ title: "LDAP", author_name: "Jerry Carter", author_email_address: "jcarter@test.com" }, { title: "Rails Recipes", author_name: "Chad Fowler", author_email_address: "cfowler@test.com" }] }
80
+
81
+ it "should import hash data successfully" do
82
+ assert_difference "Topic.count", +2 do
83
+ Topic.import values, validate: false
84
+ end
85
+ end
86
+
87
+ it "should import specified hash data successfully" do
88
+ assert_difference "Topic.count", +2 do
89
+ Topic.import columns, values, validate: false
90
+ end
91
+
92
+ Topic.all.each do |t|
93
+ assert_nil t.author_email_address
94
+ end
95
+ end
96
+
97
+ context "with extra keys" do
98
+ let(:values) do
99
+ [
100
+ { title: "LDAP", author_name: "Jerry Carter" },
101
+ { title: "Rails Recipes", author_name: "Chad Fowler", author_email_address: "cfowler@test.com" } # author_email_address is unknown
102
+ ]
103
+ end
104
+
105
+ it "should fail when column names are not specified" do
106
+ err = assert_raises ArgumentError do
107
+ Topic.import values, validate: false
108
+ end
109
+
110
+ assert err.message.include? 'Extra keys: [:author_email_address]'
111
+ end
112
+
113
+ it "should succeed when column names are specified" do
114
+ assert_difference "Topic.count", +2 do
115
+ Topic.import columns, values, validate: false
116
+ end
117
+ end
118
+ end
119
+
120
+ context "with missing keys" do
121
+ let(:values) do
122
+ [
123
+ { title: "LDAP", author_name: "Jerry Carter" },
124
+ { title: "Rails Recipes" } # author_name is missing
125
+ ]
126
+ end
127
+
128
+ it "should fail when column names are not specified" do
129
+ err = assert_raises ArgumentError do
130
+ Topic.import values, validate: false
131
+ end
132
+
133
+ assert err.message.include? 'Missing keys: [:author_name]'
134
+ end
135
+
136
+ it "should fail on missing hash key from specified column names" do
137
+ err = assert_raises ArgumentError do
138
+ Topic.import %i(author_name), values, validate: false
139
+ end
140
+
141
+ assert err.message.include? 'Missing keys: [:author_name]'
142
+ end
143
+ end
144
+ end
145
+
146
+ unless ENV["SKIP_COMPOSITE_PK"]
147
+ describe "with composite primary keys" do
148
+ it "should import models successfully" do
149
+ tags = [Tag.new(tag_id: 1, publisher_id: 1, tag: 'Mystery')]
150
+
151
+ assert_difference "Tag.count", +1 do
152
+ Tag.import tags
153
+ end
154
+ end
155
+
156
+ it "should import array of values successfully" do
157
+ columns = [:tag_id, :publisher_id, :tag]
158
+ values = [[1, 1, 'Mystery'], [2, 1, 'Science']]
159
+
160
+ assert_difference "Tag.count", +2 do
161
+ Tag.import columns, values, validate: false
162
+ end
163
+ end
164
+
165
+ it "should import models that are required to belong to models with composite primary keys" do
166
+ tag = Tag.create!(tag_id: 1, publisher_id: 1, tag: 'Mystery')
167
+ valid_tag_alias = TagAlias.new(tag_id: tag.tag_id, parent_id: tag.publisher_id, alias: 'Detective')
168
+ invalid_tag_aliases = [
169
+ TagAlias.new(tag_id: nil, parent_id: nil, alias: 'Detective'),
170
+ TagAlias.new(tag_id: tag.tag_id, parent_id: nil, alias: 'Detective'),
171
+ TagAlias.new(tag_id: nil, parent_id: tag.publisher_id, alias: 'Detective'),
172
+ ]
173
+
174
+ assert_difference "TagAlias.count", +1 do
175
+ TagAlias.import [valid_tag_alias]
176
+ end
177
+ invalid_tag_aliases.each do |invalid_tag_alias|
178
+ assert_no_difference "TagAlias.count" do
179
+ TagAlias.import [invalid_tag_alias]
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ describe "with STI models" do
187
+ it "should import models successfully" do
188
+ dictionaries = [Dictionary.new(author_name: "Noah Webster", title: "Webster's Dictionary")]
189
+
190
+ assert_difference "Dictionary.count", +1 do
191
+ Dictionary.import dictionaries
192
+ end
193
+ assert_equal "Dictionary", Dictionary.last.type
194
+ end
195
+
196
+ it "should import arrays successfully" do
197
+ columns = [:author_name, :title]
198
+ values = [["Noah Webster", "Webster's Dictionary"]]
199
+
200
+ assert_difference "Dictionary.count", +1 do
201
+ Dictionary.import columns, values
202
+ end
203
+ assert_equal "Dictionary", Dictionary.last.type
45
204
  end
46
205
  end
47
206
 
48
207
  context "with :validation option" do
49
- let(:columns) { %w(title author_name) }
50
- let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
51
- let(:valid_values_with_context) { [[1111, "Jerry Carter"], [2222, "Chad Fowler"]] }
52
- let(:invalid_values) { [["The RSpec Book", ""], ["Agile+UX", ""]] }
208
+ let(:columns) { %w(title author_name content) }
209
+ let(:valid_values) { [["LDAP", "Jerry Carter", "Putting Directories to Work."], ["Rails Recipes", "Chad Fowler", "A trusted collection of solutions."]] }
210
+ let(:valid_values_with_context) { [[1111, "Jerry Carter", "1111"], [2222, "Chad Fowler", "2222"]] }
211
+ let(:invalid_values) { [["The RSpec Book", "David Chelimsky", "..."], ["Agile+UX", "", "All about Agile in UX."]] }
212
+ let(:valid_models) { valid_values.map { |title, author_name, content| Topic.new(title: title, author_name: author_name, content: content) } }
213
+ let(:invalid_models) { invalid_values.map { |title, author_name, content| Topic.new(title: title, author_name: author_name, content: content) } }
53
214
 
54
215
  context "with validation checks turned off" do
55
216
  it "should import valid data" do
@@ -84,6 +245,22 @@ describe "#import" do
84
245
  end
85
246
  end
86
247
 
248
+ it "should ignore uniqueness validators" do
249
+ Topic.import columns, valid_values
250
+ assert_difference "Topic.count", +2 do
251
+ Topic.import columns, valid_values
252
+ end
253
+ end
254
+
255
+ it "should not alter the callback chain of the model" do
256
+ attributes = columns.zip(valid_values.first).to_h
257
+ topic = Topic.new attributes
258
+ Topic.import [topic], validate: true
259
+ duplicate_topic = Topic.new attributes
260
+ Topic.import [duplicate_topic], validate: true
261
+ assert duplicate_topic.invalid?
262
+ end
263
+
87
264
  it "should not import invalid data" do
88
265
  assert_no_difference "Topic.count" do
89
266
  Topic.import columns, invalid_values, validate: true
@@ -99,7 +276,49 @@ describe "#import" do
99
276
  it "should report the failed instances" do
100
277
  results = Topic.import columns, invalid_values, validate: true
101
278
  assert_equal invalid_values.size, results.failed_instances.size
102
- results.failed_instances.each { |e| assert_kind_of Topic, e }
279
+ assert_not_equal results.failed_instances.first, results.failed_instances.last
280
+ results.failed_instances.each do |e|
281
+ assert_kind_of Topic, e
282
+ assert_equal e.errors.count, 1
283
+ end
284
+ end
285
+
286
+ it "should index the failed instances by their poistion in the set if `track_failures` is true" do
287
+ index_offset = valid_values.length
288
+ results = Topic.import columns, valid_values + invalid_values, validate: true, track_validation_failures: true
289
+ assert_equal invalid_values.size, results.failed_instances.size
290
+ invalid_values.each_with_index do |value_set, index|
291
+ assert_equal index + index_offset, results.failed_instances[index].first
292
+ assert_equal value_set.first, results.failed_instances[index].last.title
293
+ end
294
+ end
295
+
296
+ it "should set ids in valid models if adapter supports setting primary key of imported objects" do
297
+ if ActiveRecord::Base.supports_setting_primary_key_of_imported_objects?
298
+ Topic.import (invalid_models + valid_models), validate: true
299
+ assert_nil invalid_models[0].id
300
+ assert_nil invalid_models[1].id
301
+ assert_equal valid_models[0].id, Topic.all[0].id
302
+ assert_equal valid_models[1].id, Topic.all[1].id
303
+ end
304
+ end
305
+
306
+ it "should set ActiveRecord timestamps in valid models if adapter supports setting primary key of imported objects" do
307
+ if ActiveRecord::Base.supports_setting_primary_key_of_imported_objects?
308
+ Timecop.freeze(Time.at(0)) do
309
+ Topic.import (invalid_models + valid_models), validate: true
310
+ end
311
+
312
+ assert_nil invalid_models[0].created_at
313
+ assert_nil invalid_models[0].updated_at
314
+ assert_nil invalid_models[1].created_at
315
+ assert_nil invalid_models[1].updated_at
316
+
317
+ assert_equal valid_models[0].created_at, Topic.all[0].created_at
318
+ assert_equal valid_models[0].updated_at, Topic.all[0].updated_at
319
+ assert_equal valid_models[1].created_at, Topic.all[1].created_at
320
+ assert_equal valid_models[1].updated_at, Topic.all[1].updated_at
321
+ end
103
322
  end
104
323
 
105
324
  it "should import valid data when mixed with invalid data" do
@@ -108,6 +327,60 @@ describe "#import" do
108
327
  end
109
328
  assert_equal 0, Topic.where(title: invalid_values.map(&:first)).count
110
329
  end
330
+
331
+ it "should run callbacks" do
332
+ assert_no_difference "Topic.count" do
333
+ Topic.import columns, [["invalid", "Jerry Carter"]], validate: true
334
+ end
335
+ end
336
+
337
+ it "should call validation methods" do
338
+ assert_no_difference "Topic.count" do
339
+ Topic.import columns, [["validate_failed", "Jerry Carter"]], validate: true
340
+ end
341
+ end
342
+ end
343
+
344
+ context "with uniqueness validators included" do
345
+ it "should not import duplicate records" do
346
+ Topic.import columns, valid_values
347
+ assert_no_difference "Topic.count" do
348
+ Topic.import columns, valid_values, validate_uniqueness: true
349
+ end
350
+ end
351
+ end
352
+
353
+ context "when validatoring presence of belongs_to association" do
354
+ it "should not import records without foreign key" do
355
+ assert_no_difference "UserToken.count" do
356
+ UserToken.import [:token], [['12345abcdef67890']]
357
+ end
358
+ end
359
+
360
+ it "should import records with foreign key" do
361
+ assert_difference "UserToken.count", +1 do
362
+ UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
363
+ end
364
+ end
365
+
366
+ it "should not mutate the defined validations" do
367
+ UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
368
+ ut = UserToken.new
369
+ ut.valid?
370
+ assert_includes ut.errors.messages, :user
371
+ end
372
+ end
373
+ end
374
+
375
+ context "without :validation option" do
376
+ let(:columns) { %w(title author_name) }
377
+ let(:invalid_values) { [["The RSpec Book", ""], ["Agile+UX", ""]] }
378
+
379
+ it "should not import invalid data" do
380
+ assert_no_difference "Topic.count" do
381
+ result = Topic.import columns, invalid_values
382
+ assert_equal 2, result.failed_instances.size
383
+ end
111
384
  end
112
385
  end
113
386
 
@@ -163,6 +436,15 @@ describe "#import" do
163
436
  assert_equal 3, result.num_inserts if Topic.supports_import?
164
437
  end
165
438
  end
439
+
440
+ it "should accept and call an optional callable to run after each batch" do
441
+ lambda_called = 0
442
+
443
+ my_proc = ->(_row_count, _batches, _batch, _duration) { lambda_called += 1 }
444
+ Topic.import Build(10, :topics), batch_size: 4, batch_progress: my_proc
445
+
446
+ assert_equal 3, lambda_called
447
+ end
166
448
  end
167
449
 
168
450
  context "with :synchronize option" do
@@ -171,7 +453,7 @@ describe "#import" do
171
453
 
172
454
  it "doesn't reload any data (doesn't work)" do
173
455
  Topic.import new_topics, synchronize: new_topics
174
- if Topic.support_setting_primary_key_of_imported_objects?
456
+ if Topic.supports_setting_primary_key_of_imported_objects?
175
457
  assert new_topics.all?(&:persisted?), "Records should have been reloaded"
176
458
  else
177
459
  assert new_topics.all?(&:new_record?), "No record should have been reloaded"
@@ -280,6 +562,12 @@ describe "#import" do
280
562
  Topic.import [:id, :author_name, :title], [[99, "Bob Jones", "Topic 99"]]
281
563
  assert_equal 99, Topic.last.id
282
564
  end
565
+
566
+ it "ignores the recursive option" do
567
+ assert_difference "Topic.count", +1 do
568
+ Topic.import [:author_name, :title], [["David Chelimsky", "The RSpec Book"]], recursive: true
569
+ end
570
+ end
283
571
  end
284
572
 
285
573
  context "ActiveRecord timestamps" do
@@ -287,11 +575,15 @@ describe "#import" do
287
575
 
288
576
  context "when the timestamps columns are present" do
289
577
  setup do
290
- @existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc)
291
- ActiveRecord::Base.default_timezone = :utc
578
+ @existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc, updated_at: 2.years.ago.utc, updated_on: 2.years.ago.utc)
579
+ if ActiveRecord.respond_to?(:default_timezone)
580
+ ActiveRecord.default_timezone = :utc
581
+ else
582
+ ActiveRecord::Base.default_timezone = :utc
583
+ end
292
584
  Timecop.freeze(time) do
293
585
  assert_difference "Book.count", +2 do
294
- Book.import %w(title author_name publisher created_at created_on), [["LDAP", "Big Bird", "Del Rey", nil, nil], [@existing_book.title, @existing_book.author_name, @existing_book.publisher, @existing_book.created_at, @existing_book.created_on]]
586
+ Book.import %w(title author_name publisher created_at created_on updated_at updated_on), [["LDAP", "Big Bird", "Del Rey", nil, nil, nil, nil], [@existing_book.title, @existing_book.author_name, @existing_book.publisher, @existing_book.created_at, @existing_book.created_on, @existing_book.updated_at, @existing_book.updated_on]]
295
587
  end
296
588
  end
297
589
  @new_book, @existing_book = Book.last 2
@@ -320,6 +612,23 @@ describe "#import" do
320
612
  it "should set the updated_on column for new records" do
321
613
  assert_in_delta time.to_i, @new_book.updated_on.to_i, 1.second
322
614
  end
615
+
616
+ it "should not set the updated_at column for existing records" do
617
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_at.strftime("%Y:%d")
618
+ end
619
+
620
+ it "should not set the updated_on column for existing records" do
621
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_on.strftime("%Y:%d")
622
+ end
623
+
624
+ it "should not set the updated_at column on models if changed" do
625
+ timestamp = Time.now.utc
626
+ books = [
627
+ Book.new(author_name: "Foo", title: "Baz", created_at: timestamp, updated_at: timestamp)
628
+ ]
629
+ Book.import books
630
+ assert_equal timestamp.strftime("%Y:%d"), Book.last.updated_at.strftime("%Y:%d")
631
+ end
323
632
  end
324
633
 
325
634
  context "when a custom time zone is set" do
@@ -364,12 +673,12 @@ describe "#import" do
364
673
 
365
674
  context "importing through an association scope" do
366
675
  { has_many: :chapters, polymorphic: :discounts }.each do |association_type, association|
367
- let(:book) { FactoryGirl.create :book }
368
- let(:scope) { book.public_send association }
369
- let(:klass) { { chapters: Chapter, discounts: Discount }[association] }
370
- let(:column) { { chapters: :title, discounts: :amount }[association] }
371
- let(:val1) { { chapters: 'A', discounts: 5 }[association] }
372
- let(:val2) { { chapters: 'B', discounts: 6 }[association] }
676
+ book = FactoryBot.create :book
677
+ scope = book.public_send association
678
+ klass = { chapters: Chapter, discounts: Discount }[association]
679
+ column = { chapters: :title, discounts: :amount }[association]
680
+ val1 = { chapters: 'A', discounts: 5 }[association]
681
+ val2 = { chapters: 'B', discounts: 6 }[association]
373
682
 
374
683
  context "for #{association_type}" do
375
684
  it "works importing models" do
@@ -386,7 +695,40 @@ describe "#import" do
386
695
 
387
696
  assert_equal [val1, val2], scope.map(&column).sort
388
697
  end
698
+
699
+ context "for cards and decks" do
700
+ it "works when the polymorphic name is different than base class name" do
701
+ deck = Deck.create(id: 1, name: 'test')
702
+ deck.cards.import [:id, :deck_type], [[1, 'PlayingCard']]
703
+ assert_equal deck.cards.first.deck_type, "PlayingCard"
704
+ end
705
+ end
706
+
707
+ it "works importing array of hashes" do
708
+ scope.import [{ column => val1 }, { column => val2 }]
709
+
710
+ assert_equal [val1, val2], scope.map(&column).sort
711
+ end
389
712
  end
713
+
714
+ it "works with a non-standard association primary key" do
715
+ user = User.create(id: 1, name: 'Solomon')
716
+ user.user_tokens.import [:id, :token], [[5, '12345abcdef67890']]
717
+
718
+ token = UserToken.find(5)
719
+ assert_equal 'Solomon', token.user_name
720
+ end
721
+ end
722
+ end
723
+
724
+ context "importing model with polymorphic belongs_to" do
725
+ it "works without error" do
726
+ book = FactoryBot.create :book
727
+ discount = Discount.new(discountable: book)
728
+
729
+ Discount.import([discount])
730
+
731
+ assert_equal 1, Discount.count
390
732
  end
391
733
  end
392
734
 
@@ -464,6 +806,30 @@ describe "#import" do
464
806
  end
465
807
  end
466
808
 
809
+ context 'importing arrays of values with boolean fields' do
810
+ let(:columns) { [:author_name, :title, :for_sale] }
811
+
812
+ it 'should be able to coerce integers as boolean fields' do
813
+ Book.delete_all if Book.count > 0
814
+ values = [['Author #1', 'Book #1', 0], ['Author #2', 'Book #2', 1]]
815
+ assert_difference "Book.count", +2 do
816
+ Book.import columns, values
817
+ end
818
+ assert_equal false, Book.first.for_sale
819
+ assert_equal true, Book.last.for_sale
820
+ end
821
+
822
+ it 'should be able to coerce strings as boolean fields' do
823
+ Book.delete_all if Book.count > 0
824
+ values = [['Author #1', 'Book #1', 'false'], ['Author #2', 'Book #2', 'true']]
825
+ assert_difference "Book.count", +2 do
826
+ Book.import columns, values
827
+ end
828
+ assert_equal false, Book.first.for_sale
829
+ assert_equal true, Book.last.for_sale
830
+ end
831
+ end
832
+
467
833
  describe "importing when model has default_scope" do
468
834
  it "doesn't import the default scope values" do
469
835
  assert_difference "Widget.unscoped.count", +2 do
@@ -484,45 +850,145 @@ describe "#import" do
484
850
  end
485
851
 
486
852
  describe "importing serialized fields" do
487
- it "imports values for serialized fields" do
853
+ it "imports values for serialized Hash fields" do
488
854
  assert_difference "Widget.unscoped.count", +1 do
489
855
  Widget.import [:w_id, :data], [[1, { a: :b }]]
490
856
  end
491
857
  assert_equal({ a: :b }, Widget.find_by_w_id(1).data)
492
858
  end
493
859
 
494
- if ENV['AR_VERSION'].to_f >= 3.1
495
- let(:data) { { a: :b } }
496
- it "imports values for serialized JSON fields" do
497
- assert_difference "Widget.unscoped.count", +1 do
498
- Widget.import [:w_id, :json_data], [[9, data]]
860
+ it "imports values for serialized fields" do
861
+ assert_difference "Widget.unscoped.count", +1 do
862
+ Widget.import [:w_id, :unspecified_data], [[1, { a: :b }]]
863
+ end
864
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).unspecified_data)
865
+ end
866
+
867
+ it "imports values for custom coder" do
868
+ assert_difference "Widget.unscoped.count", +1 do
869
+ Widget.import [:w_id, :custom_data], [[1, { a: :b }]]
870
+ end
871
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).custom_data)
872
+ end
873
+
874
+ let(:data) { { a: :b } }
875
+ it "imports values for serialized JSON fields" do
876
+ assert_difference "Widget.unscoped.count", +1 do
877
+ Widget.import [:w_id, :json_data], [[9, data]]
878
+ end
879
+ assert_equal(data.as_json, Widget.find_by_w_id(9).json_data)
880
+ end
881
+
882
+ it "imports serialized values from saved records" do
883
+ Widget.import [:w_id, :json_data], [[1, data]]
884
+ assert_equal data.as_json, Widget.last.json_data
885
+
886
+ w = Widget.last
887
+ w.w_id = 2
888
+ Widget.import([w])
889
+ assert_equal data.as_json, Widget.last.json_data
890
+ end
891
+
892
+ context "with a store" do
893
+ it "imports serialized attributes set using accessors" do
894
+ vendors = [Vendor.new(name: 'Vendor 1', color: 'blue')]
895
+ assert_difference "Vendor.count", +1 do
896
+ Vendor.import vendors
499
897
  end
500
- assert_equal(data.as_json, Widget.find_by_w_id(9).json_data)
898
+ assert_equal('blue', Vendor.first.color)
501
899
  end
502
900
  end
503
901
  end
504
902
 
505
903
  describe "#import!" do
506
- let(:columns) { %w(title author_name) }
507
- let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
508
- let(:invalid_values) { [["Rails Recipes", "Chad Fowler"], ["The RSpec Book", ""], ["Agile+UX", ""]] }
904
+ context "with an array of unsaved model instances" do
905
+ let(:topics) { Build(2, :topics) }
906
+ let(:invalid_topics) { Build(2, :invalid_topics) }
907
+
908
+ context "with invalid data" do
909
+ it "should raise ActiveRecord::RecordInvalid" do
910
+ assert_no_difference "Topic.count" do
911
+ assert_raise ActiveRecord::RecordInvalid do
912
+ Topic.import! invalid_topics
913
+ end
914
+ end
915
+ end
916
+ end
509
917
 
510
- context "with invalid data" do
511
- it "should raise ActiveRecord::RecordInvalid" do
512
- assert_no_difference "Topic.count" do
513
- assert_raise ActiveRecord::RecordInvalid do
514
- Topic.import! columns, invalid_values
918
+ context "with valid data" do
919
+ it "should import data" do
920
+ assert_difference "Topic.count", +2 do
921
+ Topic.import! topics
922
+ end
923
+ end
924
+ end
925
+ end
926
+
927
+ context "with array of columns and array of values" do
928
+ let(:columns) { %w(title author_name) }
929
+ let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
930
+ let(:invalid_values) { [["Rails Recipes", "Chad Fowler"], ["The RSpec Book", ""], ["Agile+UX", ""]] }
931
+
932
+ context "with invalid data" do
933
+ it "should raise ActiveRecord::RecordInvalid" do
934
+ assert_no_difference "Topic.count" do
935
+ assert_raise ActiveRecord::RecordInvalid do
936
+ Topic.import! columns, invalid_values
937
+ end
938
+ end
939
+ end
940
+ end
941
+
942
+ context "with valid data" do
943
+ it "should import data" do
944
+ assert_difference "Topic.count", +2 do
945
+ Topic.import! columns, valid_values
515
946
  end
516
947
  end
517
948
  end
518
949
  end
519
950
 
520
- context "with valid data" do
951
+ context "with objects that respond to .to_sql as values" do
952
+ let(:columns) { %w(title author_name) }
953
+ let(:valid_values) { [["LDAP", Book.select("'Jerry Carter'").limit(1)], ["Rails Recipes", Book.select("'Chad Fowler'").limit(1)]] }
954
+
521
955
  it "should import data" do
522
956
  assert_difference "Topic.count", +2 do
523
957
  Topic.import! columns, valid_values
958
+ topics = Topic.all
959
+ assert_equal "Jerry Carter", topics.first.author_name
960
+ assert_equal "Chad Fowler", topics.last.author_name
524
961
  end
525
962
  end
526
963
  end
527
964
  end
965
+ describe "importing model with after_initialize callback" do
966
+ let(:columns) { %w(name size) }
967
+ let(:valid_values) { [%w("Deer", "Small"), %w("Monkey", "Medium")] }
968
+ let(:invalid_values) do
969
+ [
970
+ { name: "giraffe", size: "Large" },
971
+ { size: "Medium" } # name is missing
972
+ ]
973
+ end
974
+ context "with validation checks turned off" do
975
+ it "should import valid data" do
976
+ Animal.import(columns, valid_values, validate: false)
977
+ assert_equal 2, Animal.count
978
+ end
979
+ it "should raise ArgumentError" do
980
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: false) }
981
+ end
982
+ end
983
+
984
+ context "with validation checks turned on" do
985
+ it "should import valid data" do
986
+ Animal.import(columns, valid_values, validate: true)
987
+ assert_equal 2, Animal.count
988
+ end
989
+ it "should raise ArgumentError" do
990
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: true) }
991
+ end
992
+ end
993
+ end
528
994
  end
@@ -1,5 +1,6 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
1
+ # frozen_string_literal: true
2
2
 
3
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
3
4
  require File.expand_path(File.dirname(__FILE__) + '/../support/assertions')
4
5
  require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
5
6
 
@@ -1,5 +1,6 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
1
+ # frozen_string_literal: true
2
2
 
3
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
3
4
  require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
4
5
 
5
6
  should_support_postgresql_import_functionality
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../support/sqlite3/import_examples')
5
+
6
+ should_support_sqlite3_import_functionality