activerecord-import-uuid 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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