activerecord-import-uuid 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/.gitignore +32 -0
  2. data/.rubocop.yml +49 -0
  3. data/.rubocop_todo.yml +36 -0
  4. data/.travis.yml +52 -0
  5. data/Brewfile +3 -0
  6. data/CHANGELOG.md +87 -0
  7. data/Gemfile +54 -0
  8. data/LICENSE +56 -0
  9. data/README.markdown +101 -0
  10. data/Rakefile +66 -0
  11. data/activerecord-import.gemspec +23 -0
  12. data/benchmarks/README +32 -0
  13. data/benchmarks/benchmark.rb +67 -0
  14. data/benchmarks/lib/base.rb +138 -0
  15. data/benchmarks/lib/cli_parser.rb +106 -0
  16. data/benchmarks/lib/float.rb +15 -0
  17. data/benchmarks/lib/mysql2_benchmark.rb +19 -0
  18. data/benchmarks/lib/output_to_csv.rb +19 -0
  19. data/benchmarks/lib/output_to_html.rb +64 -0
  20. data/benchmarks/models/test_innodb.rb +3 -0
  21. data/benchmarks/models/test_memory.rb +3 -0
  22. data/benchmarks/models/test_myisam.rb +3 -0
  23. data/benchmarks/schema/mysql_schema.rb +16 -0
  24. data/gemfiles/3.2.gemfile +3 -0
  25. data/gemfiles/4.0.gemfile +3 -0
  26. data/gemfiles/4.1.gemfile +3 -0
  27. data/gemfiles/4.2.gemfile +7 -0
  28. data/gemfiles/5.0.gemfile +3 -0
  29. data/lib/activerecord-import.rb +19 -0
  30. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +9 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +6 -0
  33. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
  34. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +6 -0
  35. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
  36. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +6 -0
  37. data/lib/activerecord-import/adapters/abstract_adapter.rb +78 -0
  38. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +5 -0
  39. data/lib/activerecord-import/adapters/mysql2_adapter.rb +5 -0
  40. data/lib/activerecord-import/adapters/mysql_adapter.rb +114 -0
  41. data/lib/activerecord-import/adapters/postgresql_adapter.rb +144 -0
  42. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +51 -0
  43. data/lib/activerecord-import/base.rb +38 -0
  44. data/lib/activerecord-import/import.rb +660 -0
  45. data/lib/activerecord-import/mysql2.rb +7 -0
  46. data/lib/activerecord-import/postgresql.rb +7 -0
  47. data/lib/activerecord-import/sqlite3.rb +7 -0
  48. data/lib/activerecord-import/synchronize.rb +66 -0
  49. data/lib/activerecord-import/value_sets_parser.rb +55 -0
  50. data/lib/activerecord-import/version.rb +5 -0
  51. data/test/adapters/jdbcmysql.rb +1 -0
  52. data/test/adapters/jdbcpostgresql.rb +1 -0
  53. data/test/adapters/mysql2.rb +1 -0
  54. data/test/adapters/mysql2_makara.rb +1 -0
  55. data/test/adapters/mysql2spatial.rb +1 -0
  56. data/test/adapters/postgis.rb +1 -0
  57. data/test/adapters/postgresql.rb +1 -0
  58. data/test/adapters/postgresql_makara.rb +1 -0
  59. data/test/adapters/seamless_database_pool.rb +1 -0
  60. data/test/adapters/spatialite.rb +1 -0
  61. data/test/adapters/sqlite3.rb +1 -0
  62. data/test/database.yml.sample +52 -0
  63. data/test/import_test.rb +574 -0
  64. data/test/jdbcmysql/import_test.rb +6 -0
  65. data/test/jdbcpostgresql/import_test.rb +5 -0
  66. data/test/models/book.rb +7 -0
  67. data/test/models/chapter.rb +4 -0
  68. data/test/models/discount.rb +3 -0
  69. data/test/models/end_note.rb +4 -0
  70. data/test/models/group.rb +3 -0
  71. data/test/models/promotion.rb +3 -0
  72. data/test/models/question.rb +3 -0
  73. data/test/models/rule.rb +3 -0
  74. data/test/models/topic.rb +9 -0
  75. data/test/models/widget.rb +24 -0
  76. data/test/mysql2/import_test.rb +5 -0
  77. data/test/mysql2_makara/import_test.rb +6 -0
  78. data/test/mysqlspatial2/import_test.rb +6 -0
  79. data/test/postgis/import_test.rb +4 -0
  80. data/test/postgresql/import_test.rb +8 -0
  81. data/test/schema/generic_schema.rb +144 -0
  82. data/test/schema/mysql_schema.rb +16 -0
  83. data/test/schema/version.rb +10 -0
  84. data/test/sqlite3/import_test.rb +52 -0
  85. data/test/support/active_support/test_case_extensions.rb +70 -0
  86. data/test/support/assertions.rb +73 -0
  87. data/test/support/factories.rb +57 -0
  88. data/test/support/generate.rb +29 -0
  89. data/test/support/mysql/import_examples.rb +85 -0
  90. data/test/support/postgresql/import_examples.rb +242 -0
  91. data/test/support/shared_examples/on_duplicate_key_update.rb +103 -0
  92. data/test/support/shared_examples/recursive_import.rb +122 -0
  93. data/test/synchronize_test.rb +33 -0
  94. data/test/test_helper.rb +59 -0
  95. data/test/travis/database.yml +62 -0
  96. data/test/value_sets_bytes_parser_test.rb +93 -0
  97. data/test/value_sets_records_parser_test.rb +32 -0
  98. metadata +225 -0
@@ -0,0 +1,7 @@
1
+ warn <<-MSG
2
+ [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
+ is deprecated. Update to autorequire using 'require "activerecord-import"'. See
4
+ http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
5
+ MSG
6
+
7
+ require "activerecord-import"
@@ -0,0 +1,7 @@
1
+ warn <<-MSG
2
+ [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
+ is deprecated. Update to autorequire using 'require "activerecord-import"'. See
4
+ http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
5
+ MSG
6
+
7
+ require "activerecord-import"
@@ -0,0 +1,7 @@
1
+ warn <<-MSG
2
+ [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
+ is deprecated. Update to autorequire using 'require "activerecord-import"'. See
4
+ http://github.com/zdennis/activerecord-import/wiki/Requiring for more information
5
+ MSG
6
+
7
+ require "activerecord-import"
@@ -0,0 +1,66 @@
1
+ module ActiveRecord # :nodoc:
2
+ class Base # :nodoc:
3
+ # Synchronizes the passed in ActiveRecord instances with data
4
+ # from the database. This is like calling reload on an individual
5
+ # ActiveRecord instance but it is intended for use on multiple instances.
6
+ #
7
+ # This uses one query for all instance updates and then updates existing
8
+ # instances rather sending one query for each instance
9
+ #
10
+ # == Examples
11
+ # # Synchronizing existing models by matching on the primary key field
12
+ # posts = Post.where(author: "Zach").first
13
+ # <.. out of system changes occur to change author name from Zach to Zachary..>
14
+ # Post.synchronize posts
15
+ # posts.first.author # => "Zachary" instead of Zach
16
+ #
17
+ # # Synchronizing using custom key fields
18
+ # posts = Post.where(author: "Zach").first
19
+ # <.. out of system changes occur to change the address of author 'Zach' to 1245 Foo Ln ..>
20
+ # Post.synchronize posts, [:name] # queries on the :name column and not the :id column
21
+ # posts.first.address # => "1245 Foo Ln" instead of whatever it was
22
+ #
23
+ def self.synchronize(instances, keys = [primary_key])
24
+ return if instances.empty?
25
+
26
+ conditions = {}
27
+
28
+ key_values = keys.map { |key| instances.map(&key.to_sym) }
29
+ keys.zip(key_values).each { |key, values| conditions[key] = values }
30
+ order = keys.map { |key| "#{key} ASC" }.join(",")
31
+
32
+ klass = instances.first.class
33
+
34
+ fresh_instances = klass.where(conditions).order(order)
35
+ instances.each do |instance|
36
+ matched_instance = fresh_instances.detect do |fresh_instance|
37
+ keys.all? { |key| fresh_instance.send(key) == instance.send(key) }
38
+ end
39
+
40
+ next unless matched_instance
41
+
42
+ instance.send :clear_aggregation_cache
43
+ instance.send :clear_association_cache
44
+ instance.instance_variable_set :@attributes, matched_instance.instance_variable_get(:@attributes)
45
+
46
+ if instance.respond_to?(:clear_changes_information)
47
+ instance.clear_changes_information # Rails 4.2 and higher
48
+ else
49
+ instance.instance_variable_set :@attributes_cache, {} # Rails 4.0, 4.1
50
+ instance.changed_attributes.clear # Rails 3.2
51
+ instance.previous_changes.clear
52
+ end
53
+
54
+ # Since the instance now accurately reflects the record in
55
+ # the database, ensure that instance.persisted? is true.
56
+ instance.instance_variable_set '@new_record', false
57
+ instance.instance_variable_set '@destroyed', false
58
+ end
59
+ end
60
+
61
+ # See ActiveRecord::ConnectionAdapters::AbstractAdapter.synchronize
62
+ def synchronize(instances, key = [ActiveRecord::Base.primary_key])
63
+ self.class.synchronize(instances, key)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ module ActiveRecord::Import
2
+ class ValueSetsBytesParser
3
+ attr_reader :reserved_bytes, :max_bytes, :values
4
+
5
+ def self.parse(values, options)
6
+ new(values, options).parse
7
+ end
8
+
9
+ def initialize(values, options)
10
+ @values = values
11
+ @reserved_bytes = options[:reserved_bytes]
12
+ @max_bytes = options[:max_bytes]
13
+ end
14
+
15
+ def parse
16
+ value_sets = []
17
+ arr = []
18
+ current_size = 0
19
+ values.each_with_index do |val, i|
20
+ comma_bytes = arr.size
21
+ bytes_thus_far = reserved_bytes + current_size + val.bytesize + comma_bytes
22
+ if bytes_thus_far <= max_bytes
23
+ current_size += val.bytesize
24
+ arr << val
25
+ else
26
+ value_sets << arr
27
+ arr = [val]
28
+ current_size = val.bytesize
29
+ end
30
+
31
+ # if we're on the last iteration push whatever we have in arr to value_sets
32
+ value_sets << arr if i == (values.size - 1)
33
+ end
34
+
35
+ [*value_sets]
36
+ end
37
+ end
38
+
39
+ class ValueSetsRecordsParser
40
+ attr_reader :max_records, :values
41
+
42
+ def self.parse(values, options)
43
+ new(values, options).parse
44
+ end
45
+
46
+ def initialize(values, options)
47
+ @values = values
48
+ @max_records = options[:max_records]
49
+ end
50
+
51
+ def parse
52
+ @values.in_groups_of(max_records, false)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Import
3
+ VERSION = "0.1"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "jdbcmysql"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "jdbcpostgresql"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "mysql2"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "mysql2_makara"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "mysql2spatial"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "postgis"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "postgresql"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "postgresql"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "seamless_database_pool"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "spatialite"
@@ -0,0 +1 @@
1
+ ENV["ARE_DB"] = "sqlite3"
@@ -0,0 +1,52 @@
1
+ common: &common
2
+ username: root
3
+ password:
4
+ encoding: utf8
5
+ host: localhost
6
+ database: activerecord_import_test
7
+
8
+ mysql2: &mysql2
9
+ <<: *common
10
+ adapter: mysql2
11
+
12
+ mysql2spatial:
13
+ <<: *mysql2
14
+
15
+ mysql2_makara:
16
+ <<: *mysql2
17
+
18
+ postgresql: &postgresql
19
+ <<: *common
20
+ username: postgres
21
+ adapter: postgresql
22
+ min_messages: warning
23
+
24
+ postresql_makara:
25
+ <<: *postgresql
26
+
27
+ postgis:
28
+ <<: *postgresql
29
+
30
+ oracle:
31
+ <<: *common
32
+ adapter: oracle
33
+ min_messages: debug
34
+
35
+ seamless_database_pool:
36
+ <<: *common
37
+ adapter: seamless_database_pool
38
+ prepared_statements: false
39
+ pool_adapter: mysql2
40
+ master:
41
+ host: localhost
42
+
43
+ sqlite:
44
+ adapter: sqlite
45
+ dbfile: test.db
46
+
47
+ sqlite3: &sqlite3
48
+ adapter: sqlite3
49
+ database: test.db
50
+
51
+ spatialite:
52
+ <<: *sqlite3
@@ -0,0 +1,574 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "#import" do
4
+ it "should return the number of inserts performed" do
5
+ # see ActiveRecord::ConnectionAdapters::AbstractAdapter test for more specifics
6
+ assert_difference "Topic.count", +10 do
7
+ result = Topic.import Build(3, :topics)
8
+ assert result.num_inserts > 0
9
+
10
+ result = Topic.import Build(7, :topics)
11
+ assert result.num_inserts > 0
12
+ end
13
+ end
14
+
15
+ it "should not produce an error when importing empty arrays" do
16
+ assert_nothing_raised do
17
+ Topic.import []
18
+ Topic.import %w(title author_name), []
19
+ end
20
+ end
21
+
22
+ describe "argument safety" do
23
+ it "should not modify the passed in columns array" do
24
+ assert_nothing_raised do
25
+ columns = %w(title author_name).freeze
26
+ Topic.import columns, [%w(foo bar)]
27
+ end
28
+ end
29
+
30
+ it "should not modify the passed in values array" do
31
+ assert_nothing_raised do
32
+ record = %w(foo bar).freeze
33
+ values = [record].freeze
34
+ Topic.import %w(title author_name), values
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "with non-default ActiveRecord models" do
40
+ context "that have a non-standard primary key (that is no sequence)" do
41
+ it "should import models successfully" do
42
+ assert_difference "Widget.count", +3 do
43
+ Widget.import Build(3, :widgets)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ context "with :validation option" do
50
+ let(:columns) { %w(title author_name) }
51
+ let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
52
+ let(:valid_values_with_context) { [[1111, "Jerry Carter"], [2222, "Chad Fowler"]] }
53
+ let(:invalid_values) { [["The RSpec Book", ""], ["Agile+UX", ""]] }
54
+
55
+ context "with validation checks turned off" do
56
+ it "should import valid data" do
57
+ assert_difference "Topic.count", +2 do
58
+ Topic.import columns, valid_values, validate: false
59
+ end
60
+ end
61
+
62
+ it "should import invalid data" do
63
+ assert_difference "Topic.count", +2 do
64
+ Topic.import columns, invalid_values, validate: false
65
+ end
66
+ end
67
+
68
+ it 'should raise a specific error if a column does not exist' do
69
+ assert_raises ActiveRecord::Import::MissingColumnError do
70
+ Topic.import ['foo'], [['bar']], validate: false
71
+ end
72
+ end
73
+ end
74
+
75
+ context "with validation checks turned on" do
76
+ it "should import valid data" do
77
+ assert_difference "Topic.count", +2 do
78
+ Topic.import columns, valid_values, validate: true
79
+ end
80
+ end
81
+
82
+ it "should import valid data with on option" do
83
+ assert_difference "Topic.count", +2 do
84
+ Topic.import columns, valid_values_with_context, validate_with_context: :context_test
85
+ end
86
+ end
87
+
88
+ it "should not import invalid data" do
89
+ assert_no_difference "Topic.count" do
90
+ Topic.import columns, invalid_values, validate: true
91
+ end
92
+ end
93
+
94
+ it "should import invalid data with on option" do
95
+ assert_no_difference "Topic.count" do
96
+ Topic.import columns, valid_values, validate_with_context: :context_test
97
+ end
98
+ end
99
+
100
+ it "should report the failed instances" do
101
+ results = Topic.import columns, invalid_values, validate: true
102
+ assert_equal invalid_values.size, results.failed_instances.size
103
+ results.failed_instances.each { |e| assert_kind_of Topic, e }
104
+ end
105
+
106
+ it "should import valid data when mixed with invalid data" do
107
+ assert_difference "Topic.count", +2 do
108
+ Topic.import columns, valid_values + invalid_values, validate: true
109
+ end
110
+ assert_equal 0, Topic.where(title: invalid_values.map(&:first)).count
111
+ end
112
+ end
113
+ end
114
+
115
+ context "with :all_or_none option" do
116
+ let(:columns) { %w(title author_name) }
117
+ let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
118
+ let(:invalid_values) { [["The RSpec Book", ""], ["Agile+UX", ""]] }
119
+ let(:mixed_values) { valid_values + invalid_values }
120
+
121
+ context "with validation checks turned on" do
122
+ it "should import valid data" do
123
+ assert_difference "Topic.count", +2 do
124
+ Topic.import columns, valid_values, all_or_none: true
125
+ end
126
+ end
127
+
128
+ it "should not import invalid data" do
129
+ assert_no_difference "Topic.count" do
130
+ Topic.import columns, invalid_values, all_or_none: true
131
+ end
132
+ end
133
+
134
+ it "should not import valid data when mixed with invalid data" do
135
+ assert_no_difference "Topic.count" do
136
+ Topic.import columns, mixed_values, all_or_none: true
137
+ end
138
+ end
139
+
140
+ it "should report the failed instances" do
141
+ results = Topic.import columns, mixed_values, all_or_none: true
142
+ assert_equal invalid_values.size, results.failed_instances.size
143
+ results.failed_instances.each { |e| assert_kind_of Topic, e }
144
+ end
145
+
146
+ it "should report the zero inserts" do
147
+ results = Topic.import columns, mixed_values, all_or_none: true
148
+ assert_equal 0, results.num_inserts
149
+ end
150
+ end
151
+ end
152
+
153
+ context "with :batch_size option" do
154
+ it "should import with a single insert" do
155
+ assert_difference "Topic.count", +10 do
156
+ result = Topic.import Build(10, :topics), batch_size: 10
157
+ assert_equal 1, result.num_inserts if Topic.supports_import?
158
+ end
159
+ end
160
+
161
+ it "should import with multiple inserts" do
162
+ assert_difference "Topic.count", +10 do
163
+ result = Topic.import Build(10, :topics), batch_size: 4
164
+ assert_equal 3, result.num_inserts if Topic.supports_import?
165
+ end
166
+ end
167
+ end
168
+
169
+ context "with :synchronize option" do
170
+ context "synchronizing on new records" do
171
+ let(:new_topics) { Build(3, :topics) }
172
+
173
+ it "doesn't reload any data (doesn't work)" do
174
+ Topic.import new_topics, synchronize: new_topics
175
+ if Topic.support_setting_primary_key_of_imported_objects?
176
+ assert new_topics.all?(&:persisted?), "Records should have been reloaded"
177
+ else
178
+ assert new_topics.all?(&:new_record?), "No record should have been reloaded"
179
+ end
180
+ end
181
+ end
182
+
183
+ context "synchronizing on new records with explicit conditions" do
184
+ let(:new_topics) { Build(3, :topics) }
185
+
186
+ it "reloads data for existing in-memory instances" do
187
+ Topic.import(new_topics, synchronize: new_topics, synchronize_keys: [:title] )
188
+ assert new_topics.all?(&:persisted?), "Records should have been reloaded"
189
+ end
190
+ end
191
+
192
+ context "synchronizing on destroyed records with explicit conditions" do
193
+ let(:new_topics) { Generate(3, :topics) }
194
+
195
+ it "reloads data for existing in-memory instances" do
196
+ new_topics.each(&:destroy)
197
+ Topic.import(new_topics, synchronize: new_topics, synchronize_keys: [:title] )
198
+ assert new_topics.all?(&:persisted?), "Records should have been reloaded"
199
+ end
200
+ end
201
+ end
202
+
203
+ context "with an array of unsaved model instances" do
204
+ let(:topic) { Build(:topic, title: "The RSpec Book", author_name: "David Chelimsky") }
205
+ let(:topics) { Build(9, :topics) }
206
+ let(:invalid_topics) { Build(7, :invalid_topics) }
207
+
208
+ it "should import records based on those model's attributes" do
209
+ assert_difference "Topic.count", +9 do
210
+ Topic.import topics
211
+ end
212
+
213
+ Topic.import [topic]
214
+ assert Topic.where(title: "The RSpec Book", author_name: "David Chelimsky").first
215
+ end
216
+
217
+ it "should not overwrite existing records" do
218
+ topic = Generate(:topic, title: "foobar")
219
+ assert_no_difference "Topic.count" do
220
+ begin
221
+ Topic.transaction do
222
+ topic.title = "baz"
223
+ Topic.import [topic]
224
+ end
225
+ rescue Exception
226
+ # PostgreSQL raises PgError due to key constraints
227
+ # I don't know why ActiveRecord doesn't catch these. *sigh*
228
+ end
229
+ end
230
+ assert_equal "foobar", topic.reload.title
231
+ end
232
+
233
+ context "with validation checks turned on" do
234
+ it "should import valid models" do
235
+ assert_difference "Topic.count", +9 do
236
+ Topic.import topics, validate: true
237
+ end
238
+ end
239
+
240
+ it "should not import invalid models" do
241
+ assert_no_difference "Topic.count" do
242
+ Topic.import invalid_topics, validate: true
243
+ end
244
+ end
245
+ end
246
+
247
+ context "with validation checks turned off" do
248
+ it "should import invalid models" do
249
+ assert_difference "Topic.count", +7 do
250
+ Topic.import invalid_topics, validate: false
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ context "with an array of columns and an array of unsaved model instances" do
257
+ let(:topics) { Build(2, :topics) }
258
+
259
+ it "should import records populating the supplied columns with the corresponding model instance attributes" do
260
+ assert_difference "Topic.count", +2 do
261
+ Topic.import [:author_name, :title], topics
262
+ end
263
+
264
+ # imported topics should be findable by their imported attributes
265
+ assert Topic.where(author_name: topics.first.author_name).first
266
+ assert Topic.where(author_name: topics.last.author_name).first
267
+ end
268
+
269
+ it "should not populate fields for columns not imported" do
270
+ topics.first.author_email_address = "zach.dennis@gmail.com"
271
+ assert_difference "Topic.count", +2 do
272
+ Topic.import [:author_name, :title], topics
273
+ end
274
+
275
+ assert !Topic.where(author_email_address: "zach.dennis@gmail.com").first
276
+ end
277
+ end
278
+
279
+ context "with an array of columns and an array of values" do
280
+ it "should import ids when specified" do
281
+ Topic.import [:id, :author_name, :title], [[99, "Bob Jones", "Topic 99"]]
282
+ assert_equal 99, Topic.last.id
283
+ end
284
+
285
+ it "ignores the recursive option" do
286
+ assert_difference "Topic.count", +1 do
287
+ Topic.import [:author_name, :title], [["David Chelimsky", "The RSpec Book"]], recursive: true
288
+ end
289
+ end
290
+ end
291
+
292
+ context "ActiveRecord timestamps" do
293
+ let(:time) { Chronic.parse("5 minutes ago") }
294
+
295
+ context "when the timestamps columns are present" do
296
+ setup do
297
+ @existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc)
298
+ ActiveRecord::Base.default_timezone = :utc
299
+ Timecop.freeze(time) do
300
+ assert_difference "Book.count", +2 do
301
+ 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]]
302
+ end
303
+ end
304
+ @new_book, @existing_book = Book.last 2
305
+ end
306
+
307
+ it "should set the created_at column for new records" do
308
+ assert_in_delta time.to_i, @new_book.created_at.to_i, 1.second
309
+ end
310
+
311
+ it "should set the created_on column for new records" do
312
+ assert_in_delta time.to_i, @new_book.created_on.to_i, 1.second
313
+ end
314
+
315
+ it "should not set the created_at column for existing records" do
316
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.created_at.strftime("%Y:%d")
317
+ end
318
+
319
+ it "should not set the created_on column for existing records" do
320
+ assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.created_on.strftime("%Y:%d")
321
+ end
322
+
323
+ it "should set the updated_at column for new records" do
324
+ assert_in_delta time.to_i, @new_book.updated_at.to_i, 1.second
325
+ end
326
+
327
+ it "should set the updated_on column for new records" do
328
+ assert_in_delta time.to_i, @new_book.updated_on.to_i, 1.second
329
+ end
330
+ end
331
+
332
+ context "when a custom time zone is set" do
333
+ setup do
334
+ Timecop.freeze(time) do
335
+ assert_difference "Book.count", +1 do
336
+ Book.import [:title, :author_name, :publisher], [["LDAP", "Big Bird", "Del Rey"]]
337
+ end
338
+ end
339
+ @book = Book.last
340
+ end
341
+
342
+ it "should set the created_at and created_on timestamps for new records" do
343
+ assert_in_delta time.to_i, @book.created_at.to_i, 1.second
344
+ assert_in_delta time.to_i, @book.created_on.to_i, 1.second
345
+ end
346
+
347
+ it "should set the updated_at and updated_on timestamps for new records" do
348
+ assert_in_delta time.to_i, @book.updated_at.to_i, 1.second
349
+ assert_in_delta time.to_i, @book.updated_on.to_i, 1.second
350
+ end
351
+ end
352
+ end
353
+
354
+ context "importing with database reserved words" do
355
+ let(:group) { Build(:group, order: "superx") }
356
+
357
+ it "should import just fine" do
358
+ assert_difference "Group.count", +1 do
359
+ Group.import [group]
360
+ end
361
+ assert_equal "superx", Group.first.order
362
+ end
363
+ end
364
+
365
+ context "importing a datetime field" do
366
+ it "should import a date with YYYY/MM/DD format just fine" do
367
+ Topic.import [:author_name, :title, :last_read], [["Bob Jones", "Topic 2", "2010/05/14"]]
368
+ assert_equal "2010/05/14".to_date, Topic.last.last_read.to_date
369
+ end
370
+ end
371
+
372
+ context "importing through an association scope" do
373
+ { has_many: :chapters, polymorphic: :discounts }.each do |association_type, association|
374
+ let(:book) { FactoryGirl.create :book }
375
+ let(:scope) { book.public_send association }
376
+ let(:klass) { { chapters: Chapter, discounts: Discount }[association] }
377
+ let(:column) { { chapters: :title, discounts: :amount }[association] }
378
+ let(:val1) { { chapters: 'A', discounts: 5 }[association] }
379
+ let(:val2) { { chapters: 'B', discounts: 6 }[association] }
380
+
381
+ context "for #{association_type}" do
382
+ it "works importing models" do
383
+ scope.import [
384
+ klass.new(column => val1),
385
+ klass.new(column => val2)
386
+ ]
387
+
388
+ assert_equal [val1, val2], scope.map(&column).sort
389
+ end
390
+
391
+ it "works importing array of columns and values" do
392
+ scope.import [column], [[val1], [val2]]
393
+
394
+ assert_equal [val1, val2], scope.map(&column).sort
395
+ end
396
+ end
397
+ end
398
+ end
399
+
400
+ context 'When importing models with Enum fields' do
401
+ it 'should be able to import enum fields' do
402
+ Book.delete_all if Book.count > 0
403
+ books = [
404
+ Book.new(author_name: "Foo", title: "Baz", status: 0),
405
+ Book.new(author_name: "Foo2", title: "Baz2", status: 1),
406
+ ]
407
+ Book.import books
408
+ assert_equal 2, Book.count
409
+
410
+ if ENV['AR_VERSION'].to_i >= 5.0
411
+ assert_equal 'draft', Book.first.read_attribute('status')
412
+ assert_equal 'published', Book.last.read_attribute('status')
413
+ else
414
+ assert_equal 0, Book.first.read_attribute('status')
415
+ assert_equal 1, Book.last.read_attribute('status')
416
+ end
417
+ end
418
+
419
+ it 'should be able to import enum fields with default value' do
420
+ Book.delete_all if Book.count > 0
421
+ books = [
422
+ Book.new(author_name: "Foo", title: "Baz")
423
+ ]
424
+ Book.import books
425
+ assert_equal 1, Book.count
426
+
427
+ if ENV['AR_VERSION'].to_i >= 5.0
428
+ assert_equal 'draft', Book.first.read_attribute('status')
429
+ else
430
+ assert_equal 0, Book.first.read_attribute('status')
431
+ end
432
+ end
433
+
434
+ if ENV['AR_VERSION'].to_f > 4.1
435
+ it 'should be able to import enum fields by name' do
436
+ Book.delete_all if Book.count > 0
437
+ books = [
438
+ Book.new(author_name: "Foo", title: "Baz", status: :draft),
439
+ Book.new(author_name: "Foo2", title: "Baz2", status: :published),
440
+ ]
441
+ Book.import books
442
+ assert_equal 2, Book.count
443
+
444
+ if ENV['AR_VERSION'].to_i >= 5.0
445
+ assert_equal 'draft', Book.first.read_attribute('status')
446
+ assert_equal 'published', Book.last.read_attribute('status')
447
+ else
448
+ assert_equal 0, Book.first.read_attribute('status')
449
+ assert_equal 1, Book.last.read_attribute('status')
450
+ end
451
+ end
452
+ end
453
+ end
454
+
455
+ context 'When importing arrays of values with Enum fields' do
456
+ let(:columns) { [:author_name, :title, :status] }
457
+ let(:values) { [['Author #1', 'Book #1', 0], ['Author #2', 'Book #2', 1]] }
458
+
459
+ it 'should be able to import enum fields' do
460
+ Book.delete_all if Book.count > 0
461
+ Book.import columns, values
462
+ assert_equal 2, Book.count
463
+
464
+ if ENV['AR_VERSION'].to_i >= 5.0
465
+ assert_equal 'draft', Book.first.read_attribute('status')
466
+ assert_equal 'published', Book.last.read_attribute('status')
467
+ else
468
+ assert_equal 0, Book.first.read_attribute('status')
469
+ assert_equal 1, Book.last.read_attribute('status')
470
+ end
471
+ end
472
+ end
473
+
474
+ describe "importing when model has default_scope" do
475
+ it "doesn't import the default scope values" do
476
+ assert_difference "Widget.unscoped.count", +2 do
477
+ Widget.import [:w_id], [[1], [2]]
478
+ end
479
+ default_scope_value = Widget.scope_attributes[:active]
480
+ assert_not_equal default_scope_value, Widget.unscoped.find_by_w_id(1)
481
+ assert_not_equal default_scope_value, Widget.unscoped.find_by_w_id(2)
482
+ end
483
+
484
+ it "imports columns that are a part of the default scope using the value specified" do
485
+ assert_difference "Widget.unscoped.count", +2 do
486
+ Widget.import [:w_id, :active], [[1, true], [2, false]]
487
+ end
488
+ assert_not_equal true, Widget.unscoped.find_by_w_id(1)
489
+ assert_not_equal false, Widget.unscoped.find_by_w_id(2)
490
+ end
491
+ end
492
+
493
+ describe "importing serialized fields" do
494
+ it "imports values for serialized Hash fields" do
495
+ assert_difference "Widget.unscoped.count", +1 do
496
+ Widget.import [:w_id, :data], [[1, { a: :b }]]
497
+ end
498
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).data)
499
+ end
500
+
501
+ it "imports values for serialized fields" do
502
+ assert_difference "Widget.unscoped.count", +1 do
503
+ Widget.import [:w_id, :unspecified_data], [[1, { a: :b }]]
504
+ end
505
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).unspecified_data)
506
+ end
507
+
508
+ it "imports values for custom coder" do
509
+ assert_difference "Widget.unscoped.count", +1 do
510
+ Widget.import [:w_id, :custom_data], [[1, { a: :b }]]
511
+ end
512
+ assert_equal({ a: :b }, Widget.find_by_w_id(1).custom_data)
513
+ end
514
+
515
+ if ENV['AR_VERSION'].to_f >= 3.1
516
+ let(:data) { { a: :b } }
517
+ it "imports values for serialized JSON fields" do
518
+ assert_difference "Widget.unscoped.count", +1 do
519
+ Widget.import [:w_id, :json_data], [[9, data]]
520
+ end
521
+ assert_equal(data.as_json, Widget.find_by_w_id(9).json_data)
522
+ end
523
+ end
524
+ end
525
+
526
+ describe "#import!" do
527
+ context "with an array of unsaved model instances" do
528
+ let(:topics) { Build(2, :topics) }
529
+ let(:invalid_topics) { Build(2, :invalid_topics) }
530
+
531
+ context "with invalid data" do
532
+ it "should raise ActiveRecord::RecordInvalid" do
533
+ assert_no_difference "Topic.count" do
534
+ assert_raise ActiveRecord::RecordInvalid do
535
+ Topic.import! invalid_topics
536
+ end
537
+ end
538
+ end
539
+ end
540
+
541
+ context "with valid data" do
542
+ it "should import data" do
543
+ assert_difference "Topic.count", +2 do
544
+ Topic.import! topics
545
+ end
546
+ end
547
+ end
548
+ end
549
+
550
+ context "with array of columns and array of values" do
551
+ let(:columns) { %w(title author_name) }
552
+ let(:valid_values) { [["LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
553
+ let(:invalid_values) { [["Rails Recipes", "Chad Fowler"], ["The RSpec Book", ""], ["Agile+UX", ""]] }
554
+
555
+ context "with invalid data" do
556
+ it "should raise ActiveRecord::RecordInvalid" do
557
+ assert_no_difference "Topic.count" do
558
+ assert_raise ActiveRecord::RecordInvalid do
559
+ Topic.import! columns, invalid_values
560
+ end
561
+ end
562
+ end
563
+ end
564
+
565
+ context "with valid data" do
566
+ it "should import data" do
567
+ assert_difference "Topic.count", +2 do
568
+ Topic.import! columns, valid_values
569
+ end
570
+ end
571
+ end
572
+ end
573
+ end
574
+ end