activerecord-import 2.1.0 → 2.3.0

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.markdown +9 -9
  4. data/lib/activerecord-import/active_record/adapters/mysql2_proxy_adapter.rb +8 -0
  5. data/lib/activerecord-import/active_record/adapters/postgresql_proxy_adapter.rb +8 -0
  6. data/lib/activerecord-import/active_record/adapters/sqlite3_proxy_adapter.rb +8 -0
  7. data/lib/activerecord-import/active_record/adapters/trilogy_proxy_adapter.rb +8 -0
  8. data/lib/activerecord-import/adapters/active_record_proxy_adapter.rb +19 -0
  9. data/lib/activerecord-import/adapters/mysql2_proxy_adapter.rb +9 -0
  10. data/lib/activerecord-import/adapters/mysql_adapter.rb +10 -3
  11. data/lib/activerecord-import/adapters/postgresql_adapter.rb +1 -1
  12. data/lib/activerecord-import/adapters/postgresql_proxy_adapter.rb +9 -0
  13. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +1 -1
  14. data/lib/activerecord-import/adapters/sqlite3_proxy_adapter.rb +9 -0
  15. data/lib/activerecord-import/adapters/trilogy_proxy_adapter.rb +9 -0
  16. data/lib/activerecord-import/base.rb +2 -2
  17. data/lib/activerecord-import/import.rb +243 -211
  18. data/lib/activerecord-import/version.rb +1 -1
  19. metadata +13 -200
  20. data/.github/workflows/test.yaml +0 -161
  21. data/.gitignore +0 -37
  22. data/.rubocop.yml +0 -118
  23. data/.rubocop_todo.yml +0 -30
  24. data/Brewfile +0 -5
  25. data/Dockerfile +0 -23
  26. data/Gemfile +0 -63
  27. data/Rakefile +0 -73
  28. data/activerecord-import.gemspec +0 -28
  29. data/benchmarks/README +0 -32
  30. data/benchmarks/benchmark.rb +0 -72
  31. data/benchmarks/lib/base.rb +0 -143
  32. data/benchmarks/lib/cli_parser.rb +0 -111
  33. data/benchmarks/lib/float.rb +0 -17
  34. data/benchmarks/lib/mysql2_benchmark.rb +0 -21
  35. data/benchmarks/lib/output_to_csv.rb +0 -21
  36. data/benchmarks/lib/output_to_html.rb +0 -66
  37. data/benchmarks/models/test_innodb.rb +0 -5
  38. data/benchmarks/models/test_memory.rb +0 -5
  39. data/benchmarks/models/test_myisam.rb +0 -5
  40. data/benchmarks/schema/mysql2_schema.rb +0 -18
  41. data/docker-compose.yml +0 -34
  42. data/gemfiles/5.2.gemfile +0 -4
  43. data/gemfiles/6.0.gemfile +0 -4
  44. data/gemfiles/6.1.gemfile +0 -4
  45. data/gemfiles/7.0.gemfile +0 -4
  46. data/gemfiles/7.1.gemfile +0 -3
  47. data/gemfiles/7.2.gemfile +0 -3
  48. data/gemfiles/8.0.gemfile +0 -3
  49. data/test/adapters/jdbcmysql.rb +0 -3
  50. data/test/adapters/jdbcpostgresql.rb +0 -3
  51. data/test/adapters/jdbcsqlite3.rb +0 -3
  52. data/test/adapters/makara_postgis.rb +0 -3
  53. data/test/adapters/mysql2.rb +0 -3
  54. data/test/adapters/mysql2_makara.rb +0 -3
  55. data/test/adapters/mysql2_proxy.rb +0 -3
  56. data/test/adapters/mysql2spatial.rb +0 -3
  57. data/test/adapters/postgis.rb +0 -3
  58. data/test/adapters/postgresql.rb +0 -3
  59. data/test/adapters/postgresql_makara.rb +0 -3
  60. data/test/adapters/postgresql_proxy.rb +0 -3
  61. data/test/adapters/seamless_database_pool.rb +0 -3
  62. data/test/adapters/spatialite.rb +0 -3
  63. data/test/adapters/sqlite3.rb +0 -3
  64. data/test/adapters/trilogy.rb +0 -11
  65. data/test/database.yml.sample +0 -65
  66. data/test/github/database.yml +0 -78
  67. data/test/import_test.rb +0 -970
  68. data/test/jdbcmysql/import_test.rb +0 -7
  69. data/test/jdbcpostgresql/import_test.rb +0 -6
  70. data/test/jdbcsqlite3/import_test.rb +0 -6
  71. data/test/makara_postgis/import_test.rb +0 -10
  72. data/test/models/account.rb +0 -5
  73. data/test/models/alarm.rb +0 -4
  74. data/test/models/animal.rb +0 -8
  75. data/test/models/author.rb +0 -9
  76. data/test/models/bike_maker.rb +0 -10
  77. data/test/models/book.rb +0 -18
  78. data/test/models/car.rb +0 -5
  79. data/test/models/card.rb +0 -5
  80. data/test/models/chapter.rb +0 -6
  81. data/test/models/composite_book.rb +0 -19
  82. data/test/models/composite_chapter.rb +0 -12
  83. data/test/models/customer.rb +0 -18
  84. data/test/models/deck.rb +0 -8
  85. data/test/models/dictionary.rb +0 -6
  86. data/test/models/discount.rb +0 -5
  87. data/test/models/end_note.rb +0 -6
  88. data/test/models/group.rb +0 -5
  89. data/test/models/order.rb +0 -17
  90. data/test/models/playing_card.rb +0 -4
  91. data/test/models/promotion.rb +0 -5
  92. data/test/models/question.rb +0 -5
  93. data/test/models/rule.rb +0 -5
  94. data/test/models/tag.rb +0 -12
  95. data/test/models/tag_alias.rb +0 -11
  96. data/test/models/topic.rb +0 -31
  97. data/test/models/user.rb +0 -5
  98. data/test/models/user_token.rb +0 -6
  99. data/test/models/vendor.rb +0 -9
  100. data/test/models/widget.rb +0 -33
  101. data/test/mysql2/import_test.rb +0 -7
  102. data/test/mysql2_makara/import_test.rb +0 -8
  103. data/test/mysql2_proxy/import_test.rb +0 -6
  104. data/test/mysqlspatial2/import_test.rb +0 -8
  105. data/test/postgis/import_test.rb +0 -10
  106. data/test/postgresql/import_test.rb +0 -6
  107. data/test/postgresql_proxy/import_test.rb +0 -6
  108. data/test/schema/generic_schema.rb +0 -230
  109. data/test/schema/jdbcpostgresql_schema.rb +0 -3
  110. data/test/schema/mysql2_schema.rb +0 -21
  111. data/test/schema/postgis_schema.rb +0 -3
  112. data/test/schema/postgresql_schema.rb +0 -94
  113. data/test/schema/sqlite3_schema.rb +0 -15
  114. data/test/schema/version.rb +0 -12
  115. data/test/sqlite3/import_test.rb +0 -6
  116. data/test/support/active_support/test_case_extensions.rb +0 -73
  117. data/test/support/assertions.rb +0 -75
  118. data/test/support/factories.rb +0 -66
  119. data/test/support/generate.rb +0 -31
  120. data/test/support/mysql/import_examples.rb +0 -97
  121. data/test/support/postgresql/import_examples.rb +0 -642
  122. data/test/support/shared_examples/on_duplicate_key_ignore.rb +0 -45
  123. data/test/support/shared_examples/on_duplicate_key_update.rb +0 -437
  124. data/test/support/shared_examples/recursive_import.rb +0 -352
  125. data/test/support/sqlite3/import_examples.rb +0 -232
  126. data/test/synchronize_test.rb +0 -43
  127. data/test/test_helper.rb +0 -91
  128. data/test/trilogy/import_test.rb +0 -7
  129. data/test/value_sets_bytes_parser_test.rb +0 -106
  130. data/test/value_sets_records_parser_test.rb +0 -34
data/test/import_test.rb DELETED
@@ -1,970 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('../test_helper', __FILE__)
4
-
5
- describe "#import" do
6
- it "should return the number of inserts performed" do
7
- # see ActiveRecord::ConnectionAdapters::AbstractAdapter test for more specifics
8
- assert_difference "Topic.count", +10 do
9
- result = Topic.import Build(3, :topics)
10
- assert result.num_inserts > 0
11
-
12
- result = Topic.import Build(7, :topics)
13
- assert result.num_inserts > 0
14
- end
15
- end
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
-
27
- it "should not produce an error when importing empty arrays" do
28
- assert_nothing_raised do
29
- Topic.import []
30
- Topic.import %w(title author_name), []
31
- end
32
- end
33
-
34
- describe "argument safety" do
35
- it "should not modify the passed in columns array" do
36
- assert_nothing_raised do
37
- columns = %w(title author_name).freeze
38
- Topic.import columns, [%w(foo bar)]
39
- end
40
- end
41
-
42
- it "should not modify the passed in values array" do
43
- assert_nothing_raised do
44
- record = %w(foo bar).freeze
45
- values = [record].freeze
46
- Topic.import %w(title author_name), values
47
- end
48
- end
49
- end
50
-
51
- describe "with non-default ActiveRecord models" do
52
- context "that have a non-standard primary key (that is no sequence)" do
53
- it "should import models successfully" do
54
- assert_difference "Widget.count", +3 do
55
- Widget.import Build(3, :widgets)
56
- end
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
204
- end
205
- end
206
-
207
- context "with :validation option" do
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) } }
214
-
215
- context "with validation checks turned off" do
216
- it "should import valid data" do
217
- assert_difference "Topic.count", +2 do
218
- Topic.import columns, valid_values, validate: false
219
- end
220
- end
221
-
222
- it "should import invalid data" do
223
- assert_difference "Topic.count", +2 do
224
- Topic.import columns, invalid_values, validate: false
225
- end
226
- end
227
-
228
- it 'should raise a specific error if a column does not exist' do
229
- assert_raises ActiveRecord::Import::MissingColumnError do
230
- Topic.import ['foo'], [['bar']], validate: false
231
- end
232
- end
233
- end
234
-
235
- context "with validation checks turned on" do
236
- it "should import valid data" do
237
- assert_difference "Topic.count", +2 do
238
- Topic.import columns, valid_values, validate: true
239
- end
240
- end
241
-
242
- it "should import valid data with on option" do
243
- assert_difference "Topic.count", +2 do
244
- Topic.import columns, valid_values_with_context, validate_with_context: :context_test
245
- end
246
- end
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
-
264
- it "should not import invalid data" do
265
- assert_no_difference "Topic.count" do
266
- Topic.import columns, invalid_values, validate: true
267
- end
268
- end
269
-
270
- it "should import invalid data with on option" do
271
- assert_no_difference "Topic.count" do
272
- Topic.import columns, valid_values, validate_with_context: :context_test
273
- end
274
- end
275
-
276
- it "should report the failed instances" do
277
- results = Topic.import columns, invalid_values, validate: true
278
- assert_equal invalid_values.size, results.failed_instances.size
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
322
- end
323
-
324
- it "should import valid data when mixed with invalid data" do
325
- assert_difference "Topic.count", +2 do
326
- Topic.import columns, valid_values + invalid_values, validate: true
327
- end
328
- assert_equal 0, Topic.where(title: invalid_values.map(&:first)).count
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
384
- end
385
- end
386
-
387
- context "with :all_or_none option" do
388
- let(:columns) { %w(title author_name) }
389
- let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
390
- let(:invalid_values) { [["The RSpec Book", ""], ["Agile+UX", ""]] }
391
- let(:mixed_values) { valid_values + invalid_values }
392
-
393
- context "with validation checks turned on" do
394
- it "should import valid data" do
395
- assert_difference "Topic.count", +2 do
396
- Topic.import columns, valid_values, all_or_none: true
397
- end
398
- end
399
-
400
- it "should not import invalid data" do
401
- assert_no_difference "Topic.count" do
402
- Topic.import columns, invalid_values, all_or_none: true
403
- end
404
- end
405
-
406
- it "should not import valid data when mixed with invalid data" do
407
- assert_no_difference "Topic.count" do
408
- Topic.import columns, mixed_values, all_or_none: true
409
- end
410
- end
411
-
412
- it "should report the failed instances" do
413
- results = Topic.import columns, mixed_values, all_or_none: true
414
- assert_equal invalid_values.size, results.failed_instances.size
415
- results.failed_instances.each { |e| assert_kind_of Topic, e }
416
- end
417
-
418
- it "should report the zero inserts" do
419
- results = Topic.import columns, mixed_values, all_or_none: true
420
- assert_equal 0, results.num_inserts
421
- end
422
- end
423
- end
424
-
425
- context "with :batch_size option" do
426
- it "should import with a single insert" do
427
- assert_difference "Topic.count", +10 do
428
- result = Topic.import Build(10, :topics), batch_size: 10
429
- assert_equal 1, result.num_inserts if Topic.supports_import?
430
- end
431
- end
432
-
433
- it "should import with multiple inserts" do
434
- assert_difference "Topic.count", +10 do
435
- result = Topic.import Build(10, :topics), batch_size: 4
436
- assert_equal 3, result.num_inserts if Topic.supports_import?
437
- end
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
448
- end
449
-
450
- context "with :synchronize option" do
451
- context "synchronizing on new records" do
452
- let(:new_topics) { Build(3, :topics) }
453
-
454
- it "doesn't reload any data (doesn't work)" do
455
- Topic.import new_topics, synchronize: new_topics
456
- if Topic.supports_setting_primary_key_of_imported_objects?
457
- assert new_topics.all?(&:persisted?), "Records should have been reloaded"
458
- else
459
- assert new_topics.all?(&:new_record?), "No record should have been reloaded"
460
- end
461
- end
462
- end
463
-
464
- context "synchronizing on new records with explicit conditions" do
465
- let(:new_topics) { Build(3, :topics) }
466
-
467
- it "reloads data for existing in-memory instances" do
468
- Topic.import(new_topics, synchronize: new_topics, synchronize_keys: [:title] )
469
- assert new_topics.all?(&:persisted?), "Records should have been reloaded"
470
- end
471
- end
472
-
473
- context "synchronizing on destroyed records with explicit conditions" do
474
- let(:new_topics) { Generate(3, :topics) }
475
-
476
- it "reloads data for existing in-memory instances" do
477
- new_topics.each(&:destroy)
478
- Topic.import(new_topics, synchronize: new_topics, synchronize_keys: [:title] )
479
- assert new_topics.all?(&:persisted?), "Records should have been reloaded"
480
- end
481
- end
482
- end
483
-
484
- context "with an array of unsaved model instances" do
485
- let(:topic) { Build(:topic, title: "The RSpec Book", author_name: "David Chelimsky") }
486
- let(:topics) { Build(9, :topics) }
487
- let(:invalid_topics) { Build(7, :invalid_topics) }
488
-
489
- it "should import records based on those model's attributes" do
490
- assert_difference "Topic.count", +9 do
491
- Topic.import topics
492
- end
493
-
494
- Topic.import [topic]
495
- assert Topic.where(title: "The RSpec Book", author_name: "David Chelimsky").first
496
- end
497
-
498
- it "should not overwrite existing records" do
499
- topic = Generate(:topic, title: "foobar")
500
- assert_no_difference "Topic.count" do
501
- begin
502
- Topic.transaction do
503
- topic.title = "baz"
504
- Topic.import [topic]
505
- end
506
- rescue Exception
507
- # PostgreSQL raises PgError due to key constraints
508
- # I don't know why ActiveRecord doesn't catch these. *sigh*
509
- end
510
- end
511
- assert_equal "foobar", topic.reload.title
512
- end
513
-
514
- context "with validation checks turned on" do
515
- it "should import valid models" do
516
- assert_difference "Topic.count", +9 do
517
- Topic.import topics, validate: true
518
- end
519
- end
520
-
521
- it "should not import invalid models" do
522
- assert_no_difference "Topic.count" do
523
- Topic.import invalid_topics, validate: true
524
- end
525
- end
526
- end
527
-
528
- context "with validation checks turned off" do
529
- it "should import invalid models" do
530
- assert_difference "Topic.count", +7 do
531
- Topic.import invalid_topics, validate: false
532
- end
533
- end
534
- end
535
- end
536
-
537
- context "with an array of columns and an array of unsaved model instances" do
538
- let(:topics) { Build(2, :topics) }
539
-
540
- it "should import records populating the supplied columns with the corresponding model instance attributes" do
541
- assert_difference "Topic.count", +2 do
542
- Topic.import [:author_name, :title], topics
543
- end
544
-
545
- # imported topics should be findable by their imported attributes
546
- assert Topic.where(author_name: topics.first.author_name).first
547
- assert Topic.where(author_name: topics.last.author_name).first
548
- end
549
-
550
- it "should not populate fields for columns not imported" do
551
- topics.first.author_email_address = "zach.dennis@gmail.com"
552
- assert_difference "Topic.count", +2 do
553
- Topic.import [:author_name, :title], topics
554
- end
555
-
556
- assert !Topic.where(author_email_address: "zach.dennis@gmail.com").first
557
- end
558
- end
559
-
560
- context "with an array of columns and an array of values" do
561
- it "should import ids when specified" do
562
- Topic.import [:id, :author_name, :title], [[99, "Bob Jones", "Topic 99"]]
563
- assert_equal 99, Topic.last.id
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
571
- end
572
-
573
- context "ActiveRecord timestamps" do
574
- let(:time) { Chronic.parse("5 minutes ago") }
575
-
576
- context "when the timestamps columns are present" do
577
- setup do
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
584
- Timecop.freeze(time) do
585
- assert_difference "Book.count", +2 do
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]]
587
- end
588
- end
589
- @new_book, @existing_book = Book.last 2
590
- end
591
-
592
- it "should set the created_at column for new records" do
593
- assert_in_delta time.to_i, @new_book.created_at.to_i, 1.second
594
- end
595
-
596
- it "should set the created_on column for new records" do
597
- assert_in_delta time.to_i, @new_book.created_on.to_i, 1.second
598
- end
599
-
600
- it "should not set the created_at column for existing records" do
601
- assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.created_at.strftime("%Y:%d")
602
- end
603
-
604
- it "should not set the created_on column for existing records" do
605
- assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.created_on.strftime("%Y:%d")
606
- end
607
-
608
- it "should set the updated_at column for new records" do
609
- assert_in_delta time.to_i, @new_book.updated_at.to_i, 1.second
610
- end
611
-
612
- it "should set the updated_on column for new records" do
613
- assert_in_delta time.to_i, @new_book.updated_on.to_i, 1.second
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
632
- end
633
-
634
- context "when a custom time zone is set" do
635
- setup do
636
- Timecop.freeze(time) do
637
- assert_difference "Book.count", +1 do
638
- Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
639
- end
640
- end
641
- @book = Book.last
642
- end
643
-
644
- it "should set the created_at and created_on timestamps for new records" do
645
- assert_in_delta time.to_i, @book.created_at.to_i, 1.second
646
- assert_in_delta time.to_i, @book.created_on.to_i, 1.second
647
- end
648
-
649
- it "should set the updated_at and updated_on timestamps for new records" do
650
- assert_in_delta time.to_i, @book.updated_at.to_i, 1.second
651
- assert_in_delta time.to_i, @book.updated_on.to_i, 1.second
652
- end
653
- end
654
- end
655
-
656
- context "importing with database reserved words" do
657
- let(:group) { Build(:group, order: "superx") }
658
-
659
- it "should import just fine" do
660
- assert_difference "Group.count", +1 do
661
- Group.import [group]
662
- end
663
- assert_equal "superx", Group.first.order
664
- end
665
- end
666
-
667
- context "importing a datetime field" do
668
- it "should import a date with YYYY/MM/DD format just fine" do
669
- Topic.import [:author_name, :title, :last_read], [["Bob Jones", "Topic 2", "2010/05/14"]]
670
- assert_equal "2010/05/14".to_date, Topic.last.last_read.to_date
671
- end
672
- end
673
-
674
- context "importing through an association scope" do
675
- { has_many: :chapters, polymorphic: :discounts }.each do |association_type, 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]
682
-
683
- context "for #{association_type}" do
684
- it "works importing models" do
685
- scope.import [
686
- klass.new(column => val1),
687
- klass.new(column => val2)
688
- ]
689
-
690
- assert_equal [val1, val2], scope.map(&column).sort
691
- end
692
-
693
- it "works importing array of columns and values" do
694
- scope.import [column], [[val1], [val2]]
695
-
696
- assert_equal [val1, val2], scope.map(&column).sort
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
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
732
- end
733
- end
734
-
735
- context 'When importing models with Enum fields' do
736
- it 'should be able to import enum fields' do
737
- Book.delete_all if Book.count > 0
738
- books = [
739
- Book.new(author_name: "Foo", title: "Baz", status: 0),
740
- Book.new(author_name: "Foo2", title: "Baz2", status: 1),
741
- ]
742
- Book.import books
743
- assert_equal 2, Book.count
744
- assert_equal 'draft', Book.first.read_attribute('status')
745
- assert_equal 'published', Book.last.read_attribute('status')
746
- end
747
-
748
- it 'should be able to import enum fields with default value' do
749
- Book.delete_all if Book.count > 0
750
- books = [
751
- Book.new(author_name: "Foo", title: "Baz")
752
- ]
753
- Book.import books
754
- assert_equal 1, Book.count
755
- assert_equal 'draft', Book.first.read_attribute('status')
756
- end
757
-
758
- it 'should be able to import enum fields by name' do
759
- Book.delete_all if Book.count > 0
760
- books = [
761
- Book.new(author_name: "Foo", title: "Baz", status: :draft),
762
- Book.new(author_name: "Foo2", title: "Baz2", status: :published),
763
- ]
764
- Book.import books
765
- assert_equal 2, Book.count
766
- assert_equal 'draft', Book.first.read_attribute('status')
767
- assert_equal 'published', Book.last.read_attribute('status')
768
- end
769
- end
770
-
771
- context 'When importing arrays of values with Enum fields' do
772
- let(:columns) { [:author_name, :title, :status] }
773
- let(:values) { [['Author #1', 'Book #1', 0], ['Author #2', 'Book #2', 1]] }
774
-
775
- it 'should be able to import enum fields' do
776
- Book.delete_all if Book.count > 0
777
- Book.import columns, values
778
- assert_equal 2, Book.count
779
-
780
- assert_equal 'draft', Book.first.read_attribute('status')
781
- assert_equal 'published', Book.last.read_attribute('status')
782
- end
783
- end
784
-
785
- context 'importing arrays of values with boolean fields' do
786
- let(:columns) { [:author_name, :title, :for_sale] }
787
-
788
- it 'should be able to coerce integers as boolean fields' do
789
- Book.delete_all if Book.count > 0
790
- values = [['Author #1', 'Book #1', 0], ['Author #2', 'Book #2', 1]]
791
- assert_difference "Book.count", +2 do
792
- Book.import columns, values
793
- end
794
- assert_equal false, Book.first.for_sale
795
- assert_equal true, Book.last.for_sale
796
- end
797
-
798
- it 'should be able to coerce strings as boolean fields' do
799
- Book.delete_all if Book.count > 0
800
- values = [['Author #1', 'Book #1', 'false'], ['Author #2', 'Book #2', 'true']]
801
- assert_difference "Book.count", +2 do
802
- Book.import columns, values
803
- end
804
- assert_equal false, Book.first.for_sale
805
- assert_equal true, Book.last.for_sale
806
- end
807
- end
808
-
809
- describe "importing when model has default_scope" do
810
- it "doesn't import the default scope values" do
811
- assert_difference "Widget.unscoped.count", +2 do
812
- Widget.import [:w_id], [[1], [2]]
813
- end
814
- default_scope_value = Widget.scope_attributes[:active]
815
- assert_not_equal default_scope_value, Widget.unscoped.find_by_w_id(1)
816
- assert_not_equal default_scope_value, Widget.unscoped.find_by_w_id(2)
817
- end
818
-
819
- it "imports columns that are a part of the default scope using the value specified" do
820
- assert_difference "Widget.unscoped.count", +2 do
821
- Widget.import [:w_id, :active], [[1, true], [2, false]]
822
- end
823
- assert_not_equal true, Widget.unscoped.find_by_w_id(1)
824
- assert_not_equal false, Widget.unscoped.find_by_w_id(2)
825
- end
826
- end
827
-
828
- describe "importing serialized fields" do
829
- it "imports values for serialized Hash fields" do
830
- assert_difference "Widget.unscoped.count", +1 do
831
- Widget.import [:w_id, :data], [[1, { a: :b }]]
832
- end
833
- assert_equal({ a: :b }, Widget.find_by_w_id(1).data)
834
- end
835
-
836
- it "imports values for serialized fields" do
837
- assert_difference "Widget.unscoped.count", +1 do
838
- Widget.import [:w_id, :unspecified_data], [[1, { a: :b }]]
839
- end
840
- assert_equal({ a: :b }, Widget.find_by_w_id(1).unspecified_data)
841
- end
842
-
843
- it "imports values for custom coder" do
844
- assert_difference "Widget.unscoped.count", +1 do
845
- Widget.import [:w_id, :custom_data], [[1, { a: :b }]]
846
- end
847
- assert_equal({ a: :b }, Widget.find_by_w_id(1).custom_data)
848
- end
849
-
850
- let(:data) { { a: :b } }
851
- it "imports values for serialized JSON fields" do
852
- assert_difference "Widget.unscoped.count", +1 do
853
- Widget.import [:w_id, :json_data], [[9, data]]
854
- end
855
- assert_equal(data.as_json, Widget.find_by_w_id(9).json_data)
856
- end
857
-
858
- it "imports serialized values from saved records" do
859
- Widget.import [:w_id, :json_data], [[1, data]]
860
- assert_equal data.as_json, Widget.last.json_data
861
-
862
- w = Widget.last
863
- w.w_id = 2
864
- Widget.import([w])
865
- assert_equal data.as_json, Widget.last.json_data
866
- end
867
-
868
- context "with a store" do
869
- it "imports serialized attributes set using accessors" do
870
- vendors = [Vendor.new(name: 'Vendor 1', color: 'blue')]
871
- assert_difference "Vendor.count", +1 do
872
- Vendor.import vendors
873
- end
874
- assert_equal('blue', Vendor.first.color)
875
- end
876
- end
877
- end
878
-
879
- describe "#import!" do
880
- context "with an array of unsaved model instances" do
881
- let(:topics) { Build(2, :topics) }
882
- let(:invalid_topics) { Build(2, :invalid_topics) }
883
-
884
- context "with invalid data" do
885
- it "should raise ActiveRecord::RecordInvalid" do
886
- assert_no_difference "Topic.count" do
887
- assert_raise ActiveRecord::RecordInvalid do
888
- Topic.import! invalid_topics
889
- end
890
- end
891
- end
892
- end
893
-
894
- context "with valid data" do
895
- it "should import data" do
896
- assert_difference "Topic.count", +2 do
897
- Topic.import! topics
898
- end
899
- end
900
- end
901
- end
902
-
903
- context "with array of columns and array of values" do
904
- let(:columns) { %w(title author_name) }
905
- let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
906
- let(:invalid_values) { [["Rails Recipes", "Chad Fowler"], ["The RSpec Book", ""], ["Agile+UX", ""]] }
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! columns, invalid_values
913
- end
914
- end
915
- end
916
- end
917
-
918
- context "with valid data" do
919
- it "should import data" do
920
- assert_difference "Topic.count", +2 do
921
- Topic.import! columns, valid_values
922
- end
923
- end
924
- end
925
- end
926
-
927
- context "with objects that respond to .to_sql as values" do
928
- let(:columns) { %w(title author_name) }
929
- let(:valid_values) { [["LDAP", Book.select("'Jerry Carter'").limit(1)], ["Rails Recipes", Book.select("'Chad Fowler'").limit(1)]] }
930
-
931
- it "should import data" do
932
- assert_difference "Topic.count", +2 do
933
- Topic.import! columns, valid_values
934
- topics = Topic.all
935
- assert_equal "Jerry Carter", topics.first.author_name
936
- assert_equal "Chad Fowler", topics.last.author_name
937
- end
938
- end
939
- end
940
- end
941
- describe "importing model with after_initialize callback" do
942
- let(:columns) { %w(name size) }
943
- let(:valid_values) { [%w("Deer", "Small"), %w("Monkey", "Medium")] }
944
- let(:invalid_values) do
945
- [
946
- { name: "giraffe", size: "Large" },
947
- { size: "Medium" } # name is missing
948
- ]
949
- end
950
- context "with validation checks turned off" do
951
- it "should import valid data" do
952
- Animal.import(columns, valid_values, validate: false)
953
- assert_equal 2, Animal.count
954
- end
955
- it "should raise ArgumentError" do
956
- assert_raise(ArgumentError) { Animal.import(invalid_values, validate: false) }
957
- end
958
- end
959
-
960
- context "with validation checks turned on" do
961
- it "should import valid data" do
962
- Animal.import(columns, valid_values, validate: true)
963
- assert_equal 2, Animal.count
964
- end
965
- it "should raise ArgumentError" do
966
- assert_raise(ArgumentError) { Animal.import(invalid_values, validate: true) }
967
- end
968
- end
969
- end
970
- end