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.
- checksums.yaml +5 -5
- data/.github/workflows/test.yaml +107 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +74 -8
- data/Brewfile +3 -1
- data/CHANGELOG.md +448 -2
- data/Gemfile +26 -19
- data/LICENSE +21 -56
- data/README.markdown +568 -32
- data/Rakefile +5 -1
- data/activerecord-import.gemspec +8 -7
- data/benchmarks/README +2 -2
- data/benchmarks/benchmark.rb +8 -1
- data/benchmarks/lib/base.rb +2 -0
- data/benchmarks/lib/cli_parser.rb +5 -2
- data/benchmarks/lib/float.rb +2 -0
- data/benchmarks/lib/mysql2_benchmark.rb +2 -0
- data/benchmarks/lib/output_to_csv.rb +2 -0
- data/benchmarks/lib/output_to_html.rb +4 -2
- data/benchmarks/models/test_innodb.rb +2 -0
- data/benchmarks/models/test_memory.rb +2 -0
- data/benchmarks/models/test_myisam.rb +2 -0
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
- data/gemfiles/4.2.gemfile +4 -3
- data/gemfiles/5.0.gemfile +4 -3
- data/gemfiles/5.1.gemfile +4 -0
- data/gemfiles/5.2.gemfile +4 -0
- data/gemfiles/6.0.gemfile +4 -0
- data/gemfiles/6.1.gemfile +4 -0
- data/gemfiles/7.0.gemfile +4 -0
- data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
- data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +8 -0
- data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +12 -16
- data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +35 -18
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +107 -28
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +141 -18
- data/lib/activerecord-import/base.rb +15 -8
- data/lib/activerecord-import/import.rb +642 -178
- data/lib/activerecord-import/mysql2.rb +2 -0
- data/lib/activerecord-import/postgresql.rb +2 -0
- data/lib/activerecord-import/sqlite3.rb +2 -0
- data/lib/activerecord-import/synchronize.rb +5 -3
- data/lib/activerecord-import/value_sets_parser.rb +29 -3
- data/lib/activerecord-import/version.rb +3 -1
- data/lib/activerecord-import.rb +5 -16
- data/test/adapters/jdbcmysql.rb +2 -0
- data/test/adapters/jdbcpostgresql.rb +2 -0
- data/test/adapters/jdbcsqlite3.rb +3 -0
- data/test/adapters/makara_postgis.rb +3 -0
- data/test/adapters/mysql2.rb +2 -0
- data/test/adapters/mysql2_makara.rb +2 -0
- data/test/adapters/mysql2spatial.rb +2 -0
- data/test/adapters/postgis.rb +2 -0
- data/test/adapters/postgresql.rb +2 -0
- data/test/adapters/postgresql_makara.rb +2 -0
- data/test/adapters/seamless_database_pool.rb +2 -0
- data/test/adapters/spatialite.rb +2 -0
- data/test/adapters/sqlite3.rb +2 -0
- data/test/{travis → github}/database.yml +7 -1
- data/test/import_test.rb +498 -32
- data/test/jdbcmysql/import_test.rb +2 -1
- data/test/jdbcpostgresql/import_test.rb +2 -1
- data/test/jdbcsqlite3/import_test.rb +6 -0
- data/test/makara_postgis/import_test.rb +10 -0
- data/test/models/account.rb +5 -0
- data/test/models/alarm.rb +4 -0
- data/test/models/animal.rb +8 -0
- data/test/models/bike_maker.rb +9 -0
- data/test/models/book.rb +4 -0
- data/test/models/car.rb +5 -0
- data/test/models/card.rb +5 -0
- data/test/models/chapter.rb +2 -0
- data/test/models/customer.rb +8 -0
- data/test/models/deck.rb +8 -0
- data/test/models/dictionary.rb +6 -0
- data/test/models/discount.rb +2 -0
- data/test/models/end_note.rb +2 -0
- data/test/models/group.rb +2 -0
- data/test/models/order.rb +8 -0
- data/test/models/playing_card.rb +4 -0
- data/test/models/promotion.rb +2 -0
- data/test/models/question.rb +2 -0
- data/test/models/rule.rb +2 -0
- data/test/models/tag.rb +7 -0
- data/test/models/tag_alias.rb +5 -0
- data/test/models/topic.rb +16 -0
- data/test/models/user.rb +5 -0
- data/test/models/user_token.rb +6 -0
- data/test/models/vendor.rb +9 -0
- data/test/models/widget.rb +18 -0
- data/test/mysql2/import_test.rb +2 -0
- data/test/mysql2_makara/import_test.rb +2 -0
- data/test/mysqlspatial2/import_test.rb +2 -0
- data/test/postgis/import_test.rb +6 -0
- data/test/postgresql/import_test.rb +2 -4
- data/test/schema/generic_schema.rb +88 -3
- data/test/schema/jdbcpostgresql_schema.rb +3 -0
- data/test/schema/mysql2_schema.rb +21 -0
- data/test/schema/postgis_schema.rb +3 -0
- data/test/schema/postgresql_schema.rb +63 -0
- data/test/schema/sqlite3_schema.rb +15 -0
- data/test/schema/version.rb +2 -0
- data/test/sqlite3/import_test.rb +4 -50
- data/test/support/active_support/test_case_extensions.rb +8 -1
- data/test/support/assertions.rb +2 -0
- data/test/support/factories.rb +17 -8
- data/test/support/generate.rb +10 -8
- data/test/support/mysql/import_examples.rb +17 -3
- data/test/support/postgresql/import_examples.rb +442 -9
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +45 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +278 -1
- data/test/support/shared_examples/recursive_import.rb +137 -12
- data/test/support/sqlite3/import_examples.rb +232 -0
- data/test/synchronize_test.rb +10 -0
- data/test/test_helper.rb +44 -3
- data/test/value_sets_bytes_parser_test.rb +15 -2
- data/test/value_sets_records_parser_test.rb +2 -0
- metadata +74 -22
- data/.travis.yml +0 -52
- data/gemfiles/3.2.gemfile +0 -3
- data/gemfiles/4.0.gemfile +0 -3
- data/gemfiles/4.1.gemfile +0 -3
- 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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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(
|
|
898
|
+
assert_equal('blue', Vendor.first.color)
|
|
501
899
|
end
|
|
502
900
|
end
|
|
503
901
|
end
|
|
504
902
|
|
|
505
903
|
describe "#import!" do
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|