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
data/test/import_test.rb CHANGED
@@ -12,6 +12,16 @@ describe "#import" do
12
12
  end
13
13
  end
14
14
 
15
+ it "warns you that you're using the library wrong" do
16
+ error = assert_raise(ArgumentError) { Topic.import %w(title author_name), ['Author #1', 'Book #1', 0] }
17
+ assert_equal error.message, "Last argument should be a two dimensional array '[[]]'. First element in array was a String"
18
+ end
19
+
20
+ it "warns you that you're passing more data than you ought to" do
21
+ error = assert_raise(ArgumentError) { Topic.import %w(title author_name), [['Author #1', 'Book #1', 0]] }
22
+ assert_equal error.message, "Number of values (8) exceeds number of columns (7)"
23
+ end
24
+
15
25
  it "should not produce an error when importing empty arrays" do
16
26
  assert_nothing_raised do
17
27
  Topic.import []
@@ -19,6 +29,23 @@ describe "#import" do
19
29
  end
20
30
  end
21
31
 
32
+ describe "argument safety" do
33
+ it "should not modify the passed in columns array" do
34
+ assert_nothing_raised do
35
+ columns = %w(title author_name).freeze
36
+ Topic.import columns, [%w(foo bar)]
37
+ end
38
+ end
39
+
40
+ it "should not modify the passed in values array" do
41
+ assert_nothing_raised do
42
+ record = %w(foo bar).freeze
43
+ values = [record].freeze
44
+ Topic.import %w(title author_name), values
45
+ end
46
+ end
47
+ end
48
+
22
49
  describe "with non-default ActiveRecord models" do
23
50
  context "that have a non-standard primary key (that is no sequence)" do
24
51
  it "should import models successfully" do
@@ -26,31 +53,150 @@ describe "#import" do
26
53
  Widget.import Build(3, :widgets)
27
54
  end
28
55
  end
56
+
57
+ context "with uppercase letters" do
58
+ it "should import models successfully" do
59
+ assert_difference "Car.count", +3 do
60
+ Car.import Build(3, :cars)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ context "that have no primary key" do
67
+ it "should import models successfully" do
68
+ assert_difference "Rule.count", +3 do
69
+ Rule.import Build(3, :rules)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "with an array of hashes" do
76
+ let(:columns) { [:title, :author_name] }
77
+ 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" }] }
78
+
79
+ it "should import hash data successfully" do
80
+ assert_difference "Topic.count", +2 do
81
+ Topic.import values, validate: false
82
+ end
83
+ end
84
+
85
+ it "should import specified hash data successfully" do
86
+ assert_difference "Topic.count", +2 do
87
+ Topic.import columns, values, validate: false
88
+ end
89
+
90
+ Topic.all.each do |t|
91
+ assert_nil t.author_email_address
92
+ end
93
+ end
94
+
95
+ context "with extra keys" do
96
+ let(:values) do
97
+ [
98
+ { title: "LDAP", author_name: "Jerry Carter" },
99
+ { title: "Rails Recipes", author_name: "Chad Fowler", author_email_address: "cfowler@test.com" } # author_email_address is unknown
100
+ ]
101
+ end
102
+
103
+ it "should fail when column names are not specified" do
104
+ err = assert_raises ArgumentError do
105
+ Topic.import values, validate: false
106
+ end
107
+
108
+ assert err.message.include? 'Extra keys: [:author_email_address]'
109
+ end
110
+
111
+ it "should succeed when column names are specified" do
112
+ assert_difference "Topic.count", +2 do
113
+ Topic.import columns, values, validate: false
114
+ end
115
+ end
116
+ end
117
+
118
+ context "with missing keys" do
119
+ let(:values) do
120
+ [
121
+ { title: "LDAP", author_name: "Jerry Carter" },
122
+ { title: "Rails Recipes" } # author_name is missing
123
+ ]
124
+ end
125
+
126
+ it "should fail when column names are not specified" do
127
+ err = assert_raises ArgumentError do
128
+ Topic.import values, validate: false
129
+ end
130
+
131
+ assert err.message.include? 'Missing keys: [:author_name]'
132
+ end
133
+
134
+ it "should fail on missing hash key from specified column names" do
135
+ err = assert_raises ArgumentError do
136
+ Topic.import %i(author_name), values, validate: false
137
+ end
138
+
139
+ assert err.message.include? 'Missing keys: [:author_name]'
140
+ end
141
+ end
142
+ end
143
+
144
+ unless ENV["SKIP_COMPOSITE_PK"]
145
+ describe "with composite primary keys" do
146
+ it "should import models successfully" do
147
+ tags = [Tag.new(tag_id: 1, publisher_id: 1, tag: 'Mystery')]
148
+
149
+ assert_difference "Tag.count", +1 do
150
+ Tag.import tags
151
+ end
152
+ end
153
+
154
+ it "should import array of values successfully" do
155
+ columns = [:tag_id, :publisher_id, :tag]
156
+ values = [[1, 1, 'Mystery'], [2, 1, 'Science']]
157
+
158
+ assert_difference "Tag.count", +2 do
159
+ Tag.import columns, values, validate: false
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ describe "with STI models" do
166
+ it "should import models successfully" do
167
+ dictionaries = [Dictionary.new(author_name: "Noah Webster", title: "Webster's Dictionary")]
168
+
169
+ assert_difference "Dictionary.count", +1 do
170
+ Dictionary.import dictionaries
171
+ end
172
+ assert_equal "Dictionary", Dictionary.first.type
29
173
  end
30
174
  end
31
175
 
32
176
  context "with :validation option" do
33
- let(:columns) { %w(title author_name) }
34
- let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
35
- let(:valid_values_with_context) { [[ 1111, "Jerry Carter"], [2222, "Chad Fowler"]] }
36
- let(:invalid_values) { [[ "The RSpec Book", ""], ["Agile+UX", ""]] }
177
+ let(:columns) { %w(title author_name content) }
178
+ let(:valid_values) { [["LDAP", "Jerry Carter", "Putting Directories to Work."], ["Rails Recipes", "Chad Fowler", "A trusted collection of solutions."]] }
179
+ let(:valid_values_with_context) { [[1111, "Jerry Carter", "1111"], [2222, "Chad Fowler", "2222"]] }
180
+ let(:invalid_values) { [["The RSpec Book", "David Chelimsky", "..."], ["Agile+UX", "", "All about Agile in UX."]] }
181
+ let(:valid_models) { valid_values.map { |title, author_name, content| Topic.new(title: title, author_name: author_name, content: content) } }
182
+ let(:invalid_models) { invalid_values.map { |title, author_name, content| Topic.new(title: title, author_name: author_name, content: content) } }
37
183
 
38
184
  context "with validation checks turned off" do
39
185
  it "should import valid data" do
40
186
  assert_difference "Topic.count", +2 do
41
- result = Topic.import columns, valid_values, :validate => false
187
+ Topic.import columns, valid_values, validate: false
42
188
  end
43
189
  end
44
190
 
45
191
  it "should import invalid data" do
46
192
  assert_difference "Topic.count", +2 do
47
- result = Topic.import columns, invalid_values, :validate => false
193
+ Topic.import columns, invalid_values, validate: false
48
194
  end
49
195
  end
50
196
 
51
197
  it 'should raise a specific error if a column does not exist' do
52
198
  assert_raises ActiveRecord::Import::MissingColumnError do
53
- Topic.import ['foo'], [['bar']], :validate => false
199
+ Topic.import ['foo'], [['bar']], validate: false
54
200
  end
55
201
  end
56
202
  end
@@ -58,88 +204,220 @@ describe "#import" do
58
204
  context "with validation checks turned on" do
59
205
  it "should import valid data" do
60
206
  assert_difference "Topic.count", +2 do
61
- result = Topic.import columns, valid_values, :validate => true
207
+ Topic.import columns, valid_values, validate: true
62
208
  end
63
209
  end
64
210
 
65
211
  it "should import valid data with on option" do
66
212
  assert_difference "Topic.count", +2 do
67
- result = Topic.import columns, valid_values_with_context, :validate_with_context => :context_test
213
+ Topic.import columns, valid_values_with_context, validate_with_context: :context_test
214
+ end
215
+ end
216
+
217
+ it "should ignore uniqueness validators" do
218
+ Topic.import columns, valid_values
219
+ assert_difference "Topic.count", +2 do
220
+ Topic.import columns, valid_values
68
221
  end
69
222
  end
70
223
 
224
+ it "should not alter the callback chain of the model" do
225
+ attributes = columns.zip(valid_values.first).to_h
226
+ topic = Topic.new attributes
227
+ Topic.import [topic], validate: true
228
+ duplicate_topic = Topic.new attributes
229
+ Topic.import [duplicate_topic], validate: true
230
+ assert duplicate_topic.invalid?
231
+ end
232
+
71
233
  it "should not import invalid data" do
72
234
  assert_no_difference "Topic.count" do
73
- result = Topic.import columns, invalid_values, :validate => true
235
+ Topic.import columns, invalid_values, validate: true
74
236
  end
75
237
  end
76
238
 
77
239
  it "should import invalid data with on option" do
78
240
  assert_no_difference "Topic.count" do
79
- result = Topic.import columns, valid_values, :validate_with_context => :context_test
241
+ Topic.import columns, valid_values, validate_with_context: :context_test
80
242
  end
81
243
  end
82
244
 
83
245
  it "should report the failed instances" do
84
- results = Topic.import columns, invalid_values, :validate => true
246
+ results = Topic.import columns, invalid_values, validate: true
247
+ assert_equal invalid_values.size, results.failed_instances.size
248
+ assert_not_equal results.failed_instances.first, results.failed_instances.last
249
+ results.failed_instances.each do |e|
250
+ assert_kind_of Topic, e
251
+ assert_equal e.errors.count, 1
252
+ end
253
+ end
254
+
255
+ it "should index the failed instances by their poistion in the set if `track_failures` is true" do
256
+ index_offset = valid_values.length
257
+ results = Topic.import columns, valid_values + invalid_values, validate: true, track_validation_failures: true
85
258
  assert_equal invalid_values.size, results.failed_instances.size
86
- results.failed_instances.each{ |e| assert_kind_of Topic, e }
259
+ invalid_values.each_with_index do |value_set, index|
260
+ assert_equal index + index_offset, results.failed_instances[index].first
261
+ assert_equal value_set.first, results.failed_instances[index].last.title
262
+ end
263
+ end
264
+
265
+ it "should set ids in valid models if adapter supports setting primary key of imported objects" do
266
+ if ActiveRecord::Base.supports_setting_primary_key_of_imported_objects?
267
+ Topic.import (invalid_models + valid_models), validate: true
268
+ assert_nil invalid_models[0].id
269
+ assert_nil invalid_models[1].id
270
+ assert_equal valid_models[0].id, Topic.all[0].id
271
+ assert_equal valid_models[1].id, Topic.all[1].id
272
+ end
273
+ end
274
+
275
+ it "should set ActiveRecord timestamps in valid models if adapter supports setting primary key of imported objects" do
276
+ if ActiveRecord::Base.supports_setting_primary_key_of_imported_objects?
277
+ Timecop.freeze(Time.at(0)) do
278
+ Topic.import (invalid_models + valid_models), validate: true
279
+ end
280
+
281
+ assert_nil invalid_models[0].created_at
282
+ assert_nil invalid_models[0].updated_at
283
+ assert_nil invalid_models[1].created_at
284
+ assert_nil invalid_models[1].updated_at
285
+
286
+ assert_equal valid_models[0].created_at, Topic.all[0].created_at
287
+ assert_equal valid_models[0].updated_at, Topic.all[0].updated_at
288
+ assert_equal valid_models[1].created_at, Topic.all[1].created_at
289
+ assert_equal valid_models[1].updated_at, Topic.all[1].updated_at
290
+ end
87
291
  end
88
292
 
89
293
  it "should import valid data when mixed with invalid data" do
90
294
  assert_difference "Topic.count", +2 do
91
- result = Topic.import columns, valid_values + invalid_values, :validate => true
295
+ Topic.import columns, valid_values + invalid_values, validate: true
92
296
  end
93
297
  assert_equal 0, Topic.where(title: invalid_values.map(&:first)).count
94
298
  end
299
+
300
+ it "should run callbacks" do
301
+ assert_no_difference "Topic.count" do
302
+ Topic.import columns, [["invalid", "Jerry Carter"]], validate: true
303
+ end
304
+ end
305
+
306
+ it "should call validation methods" do
307
+ assert_no_difference "Topic.count" do
308
+ Topic.import columns, [["validate_failed", "Jerry Carter"]], validate: true
309
+ end
310
+ end
311
+ end
312
+
313
+ context "with uniqueness validators included" do
314
+ it "should not import duplicate records" do
315
+ Topic.import columns, valid_values
316
+ assert_no_difference "Topic.count" do
317
+ Topic.import columns, valid_values, validate_uniqueness: true
318
+ end
319
+ end
320
+ end
321
+
322
+ context "when validatoring presence of belongs_to association" do
323
+ it "should not import records without foreign key" do
324
+ assert_no_difference "UserToken.count" do
325
+ UserToken.import [:token], [['12345abcdef67890']]
326
+ end
327
+ end
328
+
329
+ it "should import records with foreign key" do
330
+ assert_difference "UserToken.count", +1 do
331
+ UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
332
+ end
333
+ end
334
+
335
+ it "should not mutate the defined validations" do
336
+ UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
337
+ ut = UserToken.new
338
+ ut.valid?
339
+ assert_includes ut.errors.messages, :user
340
+ end
341
+ end
342
+ end
343
+
344
+ context "without :validation option" do
345
+ let(:columns) { %w(title author_name) }
346
+ let(:invalid_values) { [["The RSpec Book", ""], ["Agile+UX", ""]] }
347
+
348
+ it "should not import invalid data" do
349
+ assert_no_difference "Topic.count" do
350
+ result = Topic.import columns, invalid_values
351
+ assert_equal 2, result.failed_instances.size
352
+ end
95
353
  end
96
354
  end
97
355
 
98
356
  context "with :all_or_none option" do
99
357
  let(:columns) { %w(title author_name) }
100
- let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
101
- let(:invalid_values) { [[ "The RSpec Book", ""], ["Agile+UX", ""]] }
358
+ let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
359
+ let(:invalid_values) { [["The RSpec Book", ""], ["Agile+UX", ""]] }
102
360
  let(:mixed_values) { valid_values + invalid_values }
103
361
 
104
362
  context "with validation checks turned on" do
105
363
  it "should import valid data" do
106
364
  assert_difference "Topic.count", +2 do
107
- result = Topic.import columns, valid_values, :all_or_none => true
365
+ Topic.import columns, valid_values, all_or_none: true
108
366
  end
109
367
  end
110
368
 
111
369
  it "should not import invalid data" do
112
370
  assert_no_difference "Topic.count" do
113
- result = Topic.import columns, invalid_values, :all_or_none => true
371
+ Topic.import columns, invalid_values, all_or_none: true
114
372
  end
115
373
  end
116
374
 
117
375
  it "should not import valid data when mixed with invalid data" do
118
376
  assert_no_difference "Topic.count" do
119
- result = Topic.import columns, mixed_values, :all_or_none => true
377
+ Topic.import columns, mixed_values, all_or_none: true
120
378
  end
121
379
  end
122
380
 
123
381
  it "should report the failed instances" do
124
- results = Topic.import columns, mixed_values, :all_or_none => true
382
+ results = Topic.import columns, mixed_values, all_or_none: true
125
383
  assert_equal invalid_values.size, results.failed_instances.size
126
384
  results.failed_instances.each { |e| assert_kind_of Topic, e }
127
385
  end
128
386
 
129
387
  it "should report the zero inserts" do
130
- results = Topic.import columns, mixed_values, :all_or_none => true
388
+ results = Topic.import columns, mixed_values, all_or_none: true
131
389
  assert_equal 0, results.num_inserts
132
390
  end
133
391
  end
134
392
  end
135
393
 
394
+ context "with :batch_size option" do
395
+ it "should import with a single insert" do
396
+ assert_difference "Topic.count", +10 do
397
+ result = Topic.import Build(10, :topics), batch_size: 10
398
+ assert_equal 1, result.num_inserts if Topic.supports_import?
399
+ end
400
+ end
401
+
402
+ it "should import with multiple inserts" do
403
+ assert_difference "Topic.count", +10 do
404
+ result = Topic.import Build(10, :topics), batch_size: 4
405
+ assert_equal 3, result.num_inserts if Topic.supports_import?
406
+ end
407
+ end
408
+ end
409
+
136
410
  context "with :synchronize option" do
137
411
  context "synchronizing on new records" do
138
412
  let(:new_topics) { Build(3, :topics) }
139
413
 
140
414
  it "doesn't reload any data (doesn't work)" do
141
- Topic.import new_topics, :synchronize => new_topics
142
- assert new_topics.all?(&:new_record?), "No record should have been reloaded"
415
+ Topic.import new_topics, synchronize: new_topics
416
+ if Topic.supports_setting_primary_key_of_imported_objects?
417
+ assert new_topics.all?(&:persisted?), "Records should have been reloaded"
418
+ else
419
+ assert new_topics.all?(&:new_record?), "No record should have been reloaded"
420
+ end
143
421
  end
144
422
  end
145
423
 
@@ -147,7 +425,7 @@ describe "#import" do
147
425
  let(:new_topics) { Build(3, :topics) }
148
426
 
149
427
  it "reloads data for existing in-memory instances" do
150
- Topic.import(new_topics, :synchronize => new_topics, :synchronize_keys => [:title] )
428
+ Topic.import(new_topics, synchronize: new_topics, synchronize_keys: [:title] )
151
429
  assert new_topics.all?(&:persisted?), "Records should have been reloaded"
152
430
  end
153
431
  end
@@ -156,21 +434,21 @@ describe "#import" do
156
434
  let(:new_topics) { Generate(3, :topics) }
157
435
 
158
436
  it "reloads data for existing in-memory instances" do
159
- new_topics.each &:destroy
160
- Topic.import(new_topics, :synchronize => new_topics, :synchronize_keys => [:title] )
437
+ new_topics.each(&:destroy)
438
+ Topic.import(new_topics, synchronize: new_topics, synchronize_keys: [:title] )
161
439
  assert new_topics.all?(&:persisted?), "Records should have been reloaded"
162
440
  end
163
441
  end
164
442
  end
165
443
 
166
444
  context "with an array of unsaved model instances" do
167
- let(:topic) { Build(:topic, :title => "The RSpec Book", :author_name => "David Chelimsky")}
445
+ let(:topic) { Build(:topic, title: "The RSpec Book", author_name: "David Chelimsky") }
168
446
  let(:topics) { Build(9, :topics) }
169
- let(:invalid_topics){ Build(7, :invalid_topics)}
447
+ let(:invalid_topics) { Build(7, :invalid_topics) }
170
448
 
171
449
  it "should import records based on those model's attributes" do
172
450
  assert_difference "Topic.count", +9 do
173
- result = Topic.import topics
451
+ Topic.import topics
174
452
  end
175
453
 
176
454
  Topic.import [topic]
@@ -178,7 +456,7 @@ describe "#import" do
178
456
  end
179
457
 
180
458
  it "should not overwrite existing records" do
181
- topic = Generate(:topic, :title => "foobar")
459
+ topic = Generate(:topic, title: "foobar")
182
460
  assert_no_difference "Topic.count" do
183
461
  begin
184
462
  Topic.transaction do
@@ -196,13 +474,13 @@ describe "#import" do
196
474
  context "with validation checks turned on" do
197
475
  it "should import valid models" do
198
476
  assert_difference "Topic.count", +9 do
199
- result = Topic.import topics, :validate => true
477
+ Topic.import topics, validate: true
200
478
  end
201
479
  end
202
480
 
203
481
  it "should not import invalid models" do
204
482
  assert_no_difference "Topic.count" do
205
- result = Topic.import invalid_topics, :validate => true
483
+ Topic.import invalid_topics, validate: true
206
484
  end
207
485
  end
208
486
  end
@@ -210,7 +488,7 @@ describe "#import" do
210
488
  context "with validation checks turned off" do
211
489
  it "should import invalid models" do
212
490
  assert_difference "Topic.count", +7 do
213
- result = Topic.import invalid_topics, :validate => false
491
+ Topic.import invalid_topics, validate: false
214
492
  end
215
493
  end
216
494
  end
@@ -221,7 +499,7 @@ describe "#import" do
221
499
 
222
500
  it "should import records populating the supplied columns with the corresponding model instance attributes" do
223
501
  assert_difference "Topic.count", +2 do
224
- result = Topic.import [:author_name, :title], topics
502
+ Topic.import [:author_name, :title], topics
225
503
  end
226
504
 
227
505
  # imported topics should be findable by their imported attributes
@@ -232,7 +510,7 @@ describe "#import" do
232
510
  it "should not populate fields for columns not imported" do
233
511
  topics.first.author_email_address = "zach.dennis@gmail.com"
234
512
  assert_difference "Topic.count", +2 do
235
- result = Topic.import [:author_name, :title], topics
513
+ Topic.import [:author_name, :title], topics
236
514
  end
237
515
 
238
516
  assert !Topic.where(author_email_address: "zach.dennis@gmail.com").first
@@ -244,30 +522,38 @@ describe "#import" do
244
522
  Topic.import [:id, :author_name, :title], [[99, "Bob Jones", "Topic 99"]]
245
523
  assert_equal 99, Topic.last.id
246
524
  end
525
+
526
+ it "ignores the recursive option" do
527
+ assert_difference "Topic.count", +1 do
528
+ Topic.import [:author_name, :title], [["David Chelimsky", "The RSpec Book"]], recursive: true
529
+ end
530
+ end
247
531
  end
248
532
 
249
533
  context "ActiveRecord timestamps" do
534
+ let(:time) { Chronic.parse("5 minutes ago") }
535
+
250
536
  context "when the timestamps columns are present" do
251
537
  setup do
252
- @existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc)
538
+ @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)
253
539
  ActiveRecord::Base.default_timezone = :utc
254
- Timecop.freeze Chronic.parse("5 minutes ago") do
540
+ Timecop.freeze(time) do
255
541
  assert_difference "Book.count", +2 do
256
- result = Book.import ["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]]
542
+ 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]]
257
543
  end
258
544
  end
259
545
  @new_book, @existing_book = Book.last 2
260
546
  end
261
547
 
262
- it "should set the created_at column for new records" do
263
- assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @new_book.created_at.strftime("%H:%M")
548
+ it "should set the created_at column for new records" do
549
+ assert_in_delta time.to_i, @new_book.created_at.to_i, 1.second
264
550
  end
265
551
 
266
552
  it "should set the created_on column for new records" do
267
- assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @new_book.created_on.strftime("%H:%M")
553
+ assert_in_delta time.to_i, @new_book.created_on.to_i, 1.second
268
554
  end
269
555
 
270
- it "should not set the created_at column for existing records" do
556
+ it "should not set the created_at column for existing records" do
271
557
  assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.created_at.strftime("%Y:%d")
272
558
  end
273
559
 
@@ -276,27 +562,42 @@ describe "#import" do
276
562
  end
277
563
 
278
564
  it "should set the updated_at column for new records" do
279
- assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @new_book.updated_at.strftime("%H:%M")
565
+ assert_in_delta time.to_i, @new_book.updated_at.to_i, 1.second
280
566
  end
281
567
 
282
568
  it "should set the updated_on column for new records" do
283
- assert_equal 5.minutes.ago.utc.strftime("%H:%M"), @new_book.updated_on.strftime("%H:%M")
569
+ assert_in_delta time.to_i, @new_book.updated_on.to_i, 1.second
570
+ end
571
+
572
+ it "should not set the updated_at column for existing records" do
573
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_at.strftime("%Y:%d")
574
+ end
575
+
576
+ it "should not set the updated_on column for existing records" do
577
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_on.strftime("%Y:%d")
578
+ end
579
+
580
+ it "should not set the updated_at column on models if changed" do
581
+ timestamp = Time.now.utc
582
+ books = [
583
+ Book.new(author_name: "Foo", title: "Baz", created_at: timestamp, updated_at: timestamp)
584
+ ]
585
+ Book.import books
586
+ assert_equal timestamp.strftime("%Y:%d"), Book.last.updated_at.strftime("%Y:%d")
284
587
  end
285
588
  end
286
589
 
287
590
  context "when a custom time zone is set" do
288
- let(:time){ Chronic.parse("5 minutes ago") }
289
-
290
591
  setup do
291
592
  Timecop.freeze(time) do
292
593
  assert_difference "Book.count", +1 do
293
- result = Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
594
+ Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
294
595
  end
295
596
  end
296
597
  @book = Book.last
297
598
  end
298
599
 
299
- it "should set the created_at and created_on timestamps for new records" do
600
+ it "should set the created_at and created_on timestamps for new records" do
300
601
  assert_in_delta time.to_i, @book.created_at.to_i, 1.second
301
602
  assert_in_delta time.to_i, @book.created_on.to_i, 1.second
302
603
  end
@@ -309,11 +610,11 @@ describe "#import" do
309
610
  end
310
611
 
311
612
  context "importing with database reserved words" do
312
- let(:group) { Build(:group, :order => "superx") }
613
+ let(:group) { Build(:group, order: "superx") }
313
614
 
314
615
  it "should import just fine" do
315
616
  assert_difference "Group.count", +1 do
316
- result = Group.import [group]
617
+ Group.import [group]
317
618
  end
318
619
  assert_equal "superx", Group.first.order
319
620
  end
@@ -327,72 +628,156 @@ describe "#import" do
327
628
  end
328
629
 
329
630
  context "importing through an association scope" do
330
- [ true, false ].each do |b|
331
- context "when validation is " + (b ? "enabled" : "disabled") do
332
- it "should automatically set the foreign key column" do
333
- books = [[ "David Chelimsky", "The RSpec Book" ], [ "Chad Fowler", "Rails Recipes" ]]
334
- topic = FactoryGirl.create :topic
335
- topic.books.import [ :author_name, :title ], books, :validate => b
336
- assert_equal 2, topic.books.count
337
- assert topic.books.all? { |b| b.topic_id == topic.id }
631
+ { has_many: :chapters, polymorphic: :discounts }.each do |association_type, association|
632
+ book = FactoryBot.create :book
633
+ scope = book.public_send association
634
+ klass = { chapters: Chapter, discounts: Discount }[association]
635
+ column = { chapters: :title, discounts: :amount }[association]
636
+ val1 = { chapters: 'A', discounts: 5 }[association]
637
+ val2 = { chapters: 'B', discounts: 6 }[association]
638
+
639
+ context "for #{association_type}" do
640
+ it "works importing models" do
641
+ scope.import [
642
+ klass.new(column => val1),
643
+ klass.new(column => val2)
644
+ ]
645
+
646
+ assert_equal [val1, val2], scope.map(&column).sort
647
+ end
648
+
649
+ it "works importing array of columns and values" do
650
+ scope.import [column], [[val1], [val2]]
651
+
652
+ assert_equal [val1, val2], scope.map(&column).sort
653
+ end
654
+
655
+ it "works importing array of hashes" do
656
+ scope.import [{ column => val1 }, { column => val2 }]
657
+
658
+ assert_equal [val1, val2], scope.map(&column).sort
338
659
  end
339
660
  end
340
- end
341
661
 
342
- it "works importing models" do
343
- topic = FactoryGirl.create :topic
344
- books = [
345
- Book.new(:author_name => "Author #1", :title => "Book #1"),
346
- Book.new(:author_name => "Author #2", :title => "Book #2"),
347
- ]
348
- topic.books.import books
349
- assert_equal 2, topic.books.count
350
- assert topic.books.detect { |b| b.title == "Book #1" && b.author_name == "Author #1" }
351
- assert topic.books.detect { |b| b.title == "Book #2" && b.author_name == "Author #2" }
352
- end
662
+ it "works with a non-standard association primary key" do
663
+ user = User.create(id: 1, name: 'Solomon')
664
+ user.user_tokens.import [:id, :token], [[5, '12345abcdef67890']]
353
665
 
354
- it "works importing array of columns and values" do
355
- topic = FactoryGirl.create :topic
356
- books = [
357
- Book.new(:author_name => "Foo", :title => "Baz"),
358
- Book.new(:author_name => "Foo2", :title => "Baz2"),
359
- ]
360
- topic.books.import [:author_name, :title], [["Author #1", "Book #1"], ["Author #2", "Book #2"]]
361
- assert_equal 2, topic.books.count
362
- assert topic.books.detect { |b| b.title == "Book #1" && b.author_name == "Author #1" }
363
- assert topic.books.detect { |b| b.title == "Book #2" && b.author_name == "Author #2" }
666
+ token = UserToken.find(5)
667
+ assert_equal 'Solomon', token.user_name
668
+ end
364
669
  end
670
+ end
365
671
 
672
+ context "importing model with polymorphic belongs_to" do
673
+ it "works without error" do
674
+ book = FactoryBot.create :book
675
+ discount = Discount.new(discountable: book)
676
+
677
+ Discount.import([discount])
678
+
679
+ assert_equal 1, Discount.count
680
+ end
366
681
  end
367
682
 
368
683
  context 'When importing models with Enum fields' do
369
684
  it 'should be able to import enum fields' do
370
685
  Book.delete_all if Book.count > 0
371
686
  books = [
372
- Book.new(:author_name => "Foo", :title => "Baz", status: 0),
373
- Book.new(:author_name => "Foo2", :title => "Baz2", status: 1),
687
+ Book.new(author_name: "Foo", title: "Baz", status: 0),
688
+ Book.new(author_name: "Foo2", title: "Baz2", status: 1),
374
689
  ]
375
690
  Book.import books
376
691
  assert_equal 2, Book.count
377
- assert_equal 0, Book.first.read_attribute('status')
378
- assert_equal 1, Book.last.read_attribute('status')
692
+
693
+ if ENV['AR_VERSION'].to_i >= 5.0
694
+ assert_equal 'draft', Book.first.read_attribute('status')
695
+ assert_equal 'published', Book.last.read_attribute('status')
696
+ else
697
+ assert_equal 0, Book.first.read_attribute('status')
698
+ assert_equal 1, Book.last.read_attribute('status')
699
+ end
700
+ end
701
+
702
+ it 'should be able to import enum fields with default value' do
703
+ Book.delete_all if Book.count > 0
704
+ books = [
705
+ Book.new(author_name: "Foo", title: "Baz")
706
+ ]
707
+ Book.import books
708
+ assert_equal 1, Book.count
709
+
710
+ if ENV['AR_VERSION'].to_i >= 5.0
711
+ assert_equal 'draft', Book.first.read_attribute('status')
712
+ else
713
+ assert_equal 0, Book.first.read_attribute('status')
714
+ end
379
715
  end
380
716
 
381
- if ENV['AR_VERSION'].to_i > 4.1
717
+ if ENV['AR_VERSION'].to_f > 4.1
382
718
  it 'should be able to import enum fields by name' do
383
719
  Book.delete_all if Book.count > 0
384
720
  books = [
385
- Book.new(:author_name => "Foo", :title => "Baz", status: :draft),
386
- Book.new(:author_name => "Foo2", :title => "Baz2", status: :published),
721
+ Book.new(author_name: "Foo", title: "Baz", status: :draft),
722
+ Book.new(author_name: "Foo2", title: "Baz2", status: :published),
387
723
  ]
388
724
  Book.import books
389
725
  assert_equal 2, Book.count
726
+
727
+ if ENV['AR_VERSION'].to_i >= 5.0
728
+ assert_equal 'draft', Book.first.read_attribute('status')
729
+ assert_equal 'published', Book.last.read_attribute('status')
730
+ else
731
+ assert_equal 0, Book.first.read_attribute('status')
732
+ assert_equal 1, Book.last.read_attribute('status')
733
+ end
734
+ end
735
+ end
736
+ end
737
+
738
+ context 'When importing arrays of values with Enum fields' do
739
+ let(:columns) { [:author_name, :title, :status] }
740
+ let(:values) { [['Author #1', 'Book #1', 0], ['Author #2', 'Book #2', 1]] }
741
+
742
+ it 'should be able to import enum fields' do
743
+ Book.delete_all if Book.count > 0
744
+ Book.import columns, values
745
+ assert_equal 2, Book.count
746
+
747
+ if ENV['AR_VERSION'].to_i >= 5.0
748
+ assert_equal 'draft', Book.first.read_attribute('status')
749
+ assert_equal 'published', Book.last.read_attribute('status')
750
+ else
390
751
  assert_equal 0, Book.first.read_attribute('status')
391
752
  assert_equal 1, Book.last.read_attribute('status')
392
753
  end
393
754
  end
394
755
  end
395
756
 
757
+ context 'importing arrays of values with boolean fields' do
758
+ let(:columns) { [:author_name, :title, :for_sale] }
759
+
760
+ it 'should be able to coerce integers as boolean fields' do
761
+ Book.delete_all if Book.count > 0
762
+ values = [['Author #1', 'Book #1', 0], ['Author #2', 'Book #2', 1]]
763
+ assert_difference "Book.count", +2 do
764
+ Book.import columns, values
765
+ end
766
+ assert_equal false, Book.first.for_sale
767
+ assert_equal true, Book.last.for_sale
768
+ end
769
+
770
+ it 'should be able to coerce strings as boolean fields' do
771
+ Book.delete_all if Book.count > 0
772
+ values = [['Author #1', 'Book #1', 'false'], ['Author #2', 'Book #2', 'true']]
773
+ assert_difference "Book.count", +2 do
774
+ Book.import columns, values
775
+ end
776
+ assert_equal false, Book.first.for_sale
777
+ assert_equal true, Book.last.for_sale
778
+ end
779
+ end
780
+
396
781
  describe "importing when model has default_scope" do
397
782
  it "doesn't import the default scope values" do
398
783
  assert_difference "Widget.unscoped.count", +2 do
@@ -413,11 +798,145 @@ describe "#import" do
413
798
  end
414
799
 
415
800
  describe "importing serialized fields" do
801
+ it "imports values for serialized Hash fields" do
802
+ assert_difference "Widget.unscoped.count", +1 do
803
+ Widget.import [:w_id, :data], [[1, { a: :b }]]
804
+ end
805
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).data)
806
+ end
807
+
416
808
  it "imports values for serialized fields" do
417
809
  assert_difference "Widget.unscoped.count", +1 do
418
- Widget.import [:w_id, :data], [[1, {:a => :b}]]
810
+ Widget.import [:w_id, :unspecified_data], [[1, { a: :b }]]
811
+ end
812
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).unspecified_data)
813
+ end
814
+
815
+ it "imports values for custom coder" do
816
+ assert_difference "Widget.unscoped.count", +1 do
817
+ Widget.import [:w_id, :custom_data], [[1, { a: :b }]]
818
+ end
819
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).custom_data)
820
+ end
821
+
822
+ let(:data) { { a: :b } }
823
+ it "imports values for serialized JSON fields" do
824
+ assert_difference "Widget.unscoped.count", +1 do
825
+ Widget.import [:w_id, :json_data], [[9, data]]
826
+ end
827
+ assert_equal(data.as_json, Widget.find_by_w_id(9).json_data)
828
+ end
829
+
830
+ it "imports serialized values from saved records" do
831
+ Widget.import [:w_id, :json_data], [[1, data]]
832
+ assert_equal data.as_json, Widget.last.json_data
833
+
834
+ w = Widget.last
835
+ w.w_id = 2
836
+ Widget.import([w])
837
+ assert_equal data.as_json, Widget.last.json_data
838
+ end
839
+
840
+ context "with a store" do
841
+ it "imports serialized attributes set using accessors" do
842
+ vendors = [Vendor.new(name: 'Vendor 1', color: 'blue')]
843
+ assert_difference "Vendor.count", +1 do
844
+ Vendor.import vendors
845
+ end
846
+ assert_equal('blue', Vendor.first.color)
847
+ end
848
+ end
849
+ end
850
+
851
+ describe "#import!" do
852
+ context "with an array of unsaved model instances" do
853
+ let(:topics) { Build(2, :topics) }
854
+ let(:invalid_topics) { Build(2, :invalid_topics) }
855
+
856
+ context "with invalid data" do
857
+ it "should raise ActiveRecord::RecordInvalid" do
858
+ assert_no_difference "Topic.count" do
859
+ assert_raise ActiveRecord::RecordInvalid do
860
+ Topic.import! invalid_topics
861
+ end
862
+ end
863
+ end
864
+ end
865
+
866
+ context "with valid data" do
867
+ it "should import data" do
868
+ assert_difference "Topic.count", +2 do
869
+ Topic.import! topics
870
+ end
871
+ end
872
+ end
873
+ end
874
+
875
+ context "with array of columns and array of values" do
876
+ let(:columns) { %w(title author_name) }
877
+ let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
878
+ let(:invalid_values) { [["Rails Recipes", "Chad Fowler"], ["The RSpec Book", ""], ["Agile+UX", ""]] }
879
+
880
+ context "with invalid data" do
881
+ it "should raise ActiveRecord::RecordInvalid" do
882
+ assert_no_difference "Topic.count" do
883
+ assert_raise ActiveRecord::RecordInvalid do
884
+ Topic.import! columns, invalid_values
885
+ end
886
+ end
887
+ end
888
+ end
889
+
890
+ context "with valid data" do
891
+ it "should import data" do
892
+ assert_difference "Topic.count", +2 do
893
+ Topic.import! columns, valid_values
894
+ end
895
+ end
896
+ end
897
+ end
898
+
899
+ context "with objects that respond to .to_sql as values" do
900
+ let(:columns) { %w(title author_name) }
901
+ let(:valid_values) { [["LDAP", Book.select("'Jerry Carter'").limit(1)], ["Rails Recipes", Book.select("'Chad Fowler'").limit(1)]] }
902
+
903
+ it "should import data" do
904
+ assert_difference "Topic.count", +2 do
905
+ Topic.import! columns, valid_values
906
+ topics = Topic.all
907
+ assert_equal "Jerry Carter", topics.first.author_name
908
+ assert_equal "Chad Fowler", topics.last.author_name
909
+ end
910
+ end
911
+ end
912
+ end
913
+ describe "importing model with after_initialize callback" do
914
+ let(:columns) { %w(name size) }
915
+ let(:valid_values) { [%w("Deer", "Small"), %w("Monkey", "Medium")] }
916
+ let(:invalid_values) do
917
+ [
918
+ { name: "giraffe", size: "Large" },
919
+ { size: "Medium" } # name is missing
920
+ ]
921
+ end
922
+ context "with validation checks turned off" do
923
+ it "should import valid data" do
924
+ Animal.import(columns, valid_values, validate: false)
925
+ assert_equal 2, Animal.count
926
+ end
927
+ it "should raise ArgumentError" do
928
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: false) }
929
+ end
930
+ end
931
+
932
+ context "with validation checks turned on" do
933
+ it "should import valid data" do
934
+ Animal.import(columns, valid_values, validate: true)
935
+ assert_equal 2, Animal.count
936
+ end
937
+ it "should raise ArgumentError" do
938
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: true) }
419
939
  end
420
- assert_equal({:a => :b}, Widget.find_by_w_id(1).data)
421
940
  end
422
941
  end
423
942
  end