activerecord-import 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +49 -0
  3. data/.rubocop_todo.yml +36 -0
  4. data/.travis.yml +31 -7
  5. data/CHANGELOG.md +19 -0
  6. data/Gemfile +5 -2
  7. data/README.markdown +6 -1
  8. data/Rakefile +5 -2
  9. data/activerecord-import.gemspec +1 -1
  10. data/benchmarks/benchmark.rb +67 -68
  11. data/benchmarks/lib/base.rb +136 -137
  12. data/benchmarks/lib/cli_parser.rb +106 -107
  13. data/benchmarks/lib/mysql2_benchmark.rb +19 -21
  14. data/benchmarks/lib/output_to_csv.rb +2 -1
  15. data/benchmarks/lib/output_to_html.rb +8 -13
  16. data/benchmarks/schema/mysql_schema.rb +8 -8
  17. data/gemfiles/4.0.gemfile +1 -1
  18. data/gemfiles/4.1.gemfile +1 -1
  19. data/gemfiles/4.2.gemfile +1 -1
  20. data/gemfiles/5.0.gemfile +1 -1
  21. data/lib/activerecord-import.rb +2 -0
  22. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +0 -1
  23. data/lib/activerecord-import/adapters/abstract_adapter.rb +9 -9
  24. data/lib/activerecord-import/adapters/mysql_adapter.rb +17 -17
  25. data/lib/activerecord-import/adapters/postgresql_adapter.rb +20 -22
  26. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +9 -9
  27. data/lib/activerecord-import/base.rb +3 -3
  28. data/lib/activerecord-import/import.rb +152 -131
  29. data/lib/activerecord-import/synchronize.rb +20 -20
  30. data/lib/activerecord-import/value_sets_parser.rb +7 -6
  31. data/lib/activerecord-import/version.rb +1 -1
  32. data/test/adapters/mysql2spatial.rb +1 -1
  33. data/test/adapters/postgis.rb +1 -1
  34. data/test/adapters/postgresql.rb +1 -1
  35. data/test/adapters/spatialite.rb +1 -1
  36. data/test/adapters/sqlite3.rb +1 -1
  37. data/test/import_test.rb +121 -70
  38. data/test/models/book.rb +5 -6
  39. data/test/models/chapter.rb +2 -2
  40. data/test/models/discount.rb +3 -0
  41. data/test/models/end_note.rb +2 -2
  42. data/test/models/promotion.rb +1 -1
  43. data/test/models/question.rb +1 -1
  44. data/test/models/rule.rb +2 -2
  45. data/test/models/topic.rb +3 -3
  46. data/test/models/widget.rb +1 -1
  47. data/test/postgis/import_test.rb +1 -1
  48. data/test/schema/generic_schema.rb +100 -96
  49. data/test/schema/mysql_schema.rb +5 -7
  50. data/test/sqlite3/import_test.rb +0 -2
  51. data/test/support/active_support/test_case_extensions.rb +12 -15
  52. data/test/support/assertions.rb +1 -1
  53. data/test/support/factories.rb +15 -16
  54. data/test/support/generate.rb +4 -4
  55. data/test/support/mysql/import_examples.rb +21 -21
  56. data/test/support/postgresql/import_examples.rb +83 -55
  57. data/test/support/shared_examples/on_duplicate_key_update.rb +23 -23
  58. data/test/synchronize_test.rb +2 -2
  59. data/test/test_helper.rb +6 -8
  60. data/test/value_sets_bytes_parser_test.rb +14 -17
  61. data/test/value_sets_records_parser_test.rb +6 -6
  62. metadata +7 -4
  63. data/test/travis/build.sh +0 -34
@@ -2,15 +2,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
3
  include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
4
 
5
- MIN_VERSION_FOR_UPSERT = 90500
5
+ MIN_VERSION_FOR_UPSERT = 90_500
6
6
 
7
7
  def insert_many( sql, values, *args ) # :nodoc:
8
8
  number_of_inserts = 1
9
9
 
10
- base_sql,post_sql = if sql.is_a?( String )
11
- [ sql, '' ]
10
+ base_sql, post_sql = if sql.is_a?( String )
11
+ [sql, '']
12
12
  elsif sql.is_a?( Array )
13
- [ sql.shift, sql.join( ' ' ) ]
13
+ [sql.shift, sql.join( ' ' )]
14
14
  end
15
15
 
16
16
  sql2insert = base_sql + values.join( ',' ) + post_sql
@@ -18,7 +18,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
18
18
 
19
19
  ActiveRecord::Base.connection.query_cache.clear
20
20
 
21
- [number_of_inserts,ids]
21
+ [number_of_inserts, ids]
22
22
  end
23
23
 
24
24
  def next_value_for_sequence(sequence_name)
@@ -26,15 +26,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
26
26
  end
27
27
 
28
28
  def post_sql_statements( table_name, options ) # :nodoc:
29
- unless options[:primary_key].blank?
30
- super(table_name, options) << ("RETURNING #{options[:primary_key]}")
31
- else
29
+ if options[:primary_key].blank?
32
30
  super(table_name, options)
31
+ else
32
+ super(table_name, options) << "RETURNING #{options[:primary_key]}"
33
33
  end
34
34
  end
35
35
 
36
36
  # Add a column to be updated on duplicate key update
37
- def add_column_for_on_duplicate_key_update( column, options={} ) # :nodoc:
37
+ def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
38
38
  arg = options[:on_duplicate_key_update]
39
39
  if arg.is_a?( Hash )
40
40
  columns = arg.fetch( :columns ) { arg[:columns] = [] }
@@ -51,9 +51,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
51
51
  # in +args+.
52
52
  def sql_for_on_duplicate_key_ignore( table_name, *args ) # :nodoc:
53
53
  arg = args.first
54
- if arg.is_a?( Hash )
55
- conflict_target = sql_for_conflict_target( arg )
56
- end
54
+ conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
57
55
  " ON CONFLICT #{conflict_target}DO NOTHING"
58
56
  end
59
57
 
@@ -61,9 +59,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
61
59
  # in +args+.
62
60
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
63
61
  arg = args.first
64
- if arg.is_a?( Array ) || arg.is_a?( String )
65
- arg = { :columns => arg }
66
- end
62
+ arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
67
63
  return unless arg.is_a?( Hash )
68
64
 
69
65
  sql = " ON CONFLICT "
@@ -92,7 +88,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
92
88
  sql
93
89
  end
94
90
 
95
- def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
91
+ def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
96
92
  results = arr.map do |column|
97
93
  qc = quote_column_name( column )
98
94
  "#{qc}=EXCLUDED.#{qc}"
@@ -109,10 +105,12 @@ module ActiveRecord::Import::PostgreSQLAdapter
109
105
  results.join( ',' )
110
106
  end
111
107
 
112
- def sql_for_conflict_target( args={} )
113
- if constraint_name = args[:constraint_name]
108
+ def sql_for_conflict_target( args = {} )
109
+ constraint_name = args[:constraint_name]
110
+ conflict_target = args[:conflict_target]
111
+ if constraint_name
114
112
  "ON CONSTRAINT #{constraint_name} "
115
- elsif conflict_target = args[:conflict_target]
113
+ elsif conflict_target
116
114
  '(' << Array( conflict_target ).join( ', ' ) << ') '
117
115
  end
118
116
  end
@@ -122,15 +120,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
122
120
  end
123
121
 
124
122
  # Return true if the statement is a duplicate key record error
125
- def duplicate_key_update_error?(exception)# :nodoc:
123
+ def duplicate_key_update_error?(exception) # :nodoc:
126
124
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
127
125
  end
128
126
 
129
- def supports_on_duplicate_key_update?(current_version=self.postgresql_version)
127
+ def supports_on_duplicate_key_update?(current_version = postgresql_version)
130
128
  current_version >= MIN_VERSION_FOR_UPSERT
131
129
  end
132
130
 
133
- def supports_on_duplicate_key_ignore?(current_version=self.postgresql_version)
131
+ def supports_on_duplicate_key_ignore?(current_version = postgresql_version)
134
132
  supports_on_duplicate_key_update?(current_version)
135
133
  end
136
134
 
@@ -1,13 +1,13 @@
1
1
  module ActiveRecord::Import::SQLite3Adapter
2
2
  include ActiveRecord::Import::ImportSupport
3
3
 
4
- MIN_VERSION_FOR_IMPORT = "3.7.11"
4
+ MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
5
5
  SQLITE_LIMIT_COMPOUND_SELECT = 500
6
6
 
7
7
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
8
8
  # to ensure that we only support import in supported version of SQLite.
9
9
  # Which INSERT statements with multiple value sets was introduced in 3.7.11.
10
- def supports_import?(current_version=self.sqlite_version)
10
+ def supports_import?(current_version = sqlite_version)
11
11
  if current_version >= MIN_VERSION_FOR_IMPORT
12
12
  true
13
13
  else
@@ -19,22 +19,22 @@ module ActiveRecord::Import::SQLite3Adapter
19
19
  # elements that are in position >= 1 will be appended to the final SQL.
20
20
  def insert_many(sql, values, *args) # :nodoc:
21
21
  number_of_inserts = 0
22
- base_sql,post_sql = if sql.is_a?( String )
23
- [ sql, '' ]
22
+ base_sql, post_sql = if sql.is_a?( String )
23
+ [sql, '']
24
24
  elsif sql.is_a?( Array )
25
- [ sql.shift, sql.join( ' ' ) ]
25
+ [sql.shift, sql.join( ' ' )]
26
26
  end
27
27
 
28
28
  value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
29
- :max_records => SQLITE_LIMIT_COMPOUND_SELECT)
29
+ max_records: SQLITE_LIMIT_COMPOUND_SELECT)
30
30
 
31
- value_sets.each do |values|
31
+ value_sets.each do |value_set|
32
32
  number_of_inserts += 1
33
- sql2insert = base_sql + values.join( ',' ) + post_sql
33
+ sql2insert = base_sql + value_set.join( ',' ) + post_sql
34
34
  insert( sql2insert, *args )
35
35
  end
36
36
 
37
- [number_of_inserts,[]]
37
+ [number_of_inserts, []]
38
38
  end
39
39
 
40
40
  def next_value_for_sequence(sequence_name)
@@ -3,7 +3,7 @@ require "active_record"
3
3
  require "active_record/version"
4
4
 
5
5
  module ActiveRecord::Import
6
- AdapterPath = "activerecord-import/active_record/adapters"
6
+ ADAPTER_PATH = "activerecord-import/active_record/adapters".freeze
7
7
 
8
8
  def self.base_adapter(adapter)
9
9
  case adapter
@@ -16,9 +16,9 @@ module ActiveRecord::Import
16
16
 
17
17
  # Loads the import functionality for a specific database adapter
18
18
  def self.require_adapter(adapter)
19
- require File.join(AdapterPath,"/abstract_adapter")
19
+ require File.join(ADAPTER_PATH, "/abstract_adapter")
20
20
  begin
21
- require File.join(AdapterPath,"/#{base_adapter(adapter)}_adapter")
21
+ require File.join(ADAPTER_PATH, "/#{base_adapter(adapter)}_adapter")
22
22
  rescue LoadError
23
23
  # fallback
24
24
  end
@@ -1,10 +1,9 @@
1
1
  require "ostruct"
2
2
 
3
- module ActiveRecord::Import::ConnectionAdapters ; end
3
+ module ActiveRecord::Import::ConnectionAdapters; end
4
4
 
5
5
  module ActiveRecord::Import #:nodoc:
6
- class Result < Struct.new(:failed_instances, :num_inserts, :ids)
7
- end
6
+ Result = Struct.new(:failed_instances, :num_inserts, :ids)
8
7
 
9
8
  module ImportSupport #:nodoc:
10
9
  def supports_import? #:nodoc:
@@ -39,15 +38,15 @@ class ActiveRecord::Associations::CollectionAssociation
39
38
 
40
39
  options = args.last.is_a?(Hash) ? args.pop : {}
41
40
 
42
- model_klass = self.reflection.klass
43
- symbolized_foreign_key = self.reflection.foreign_key.to_sym
41
+ model_klass = reflection.klass
42
+ symbolized_foreign_key = reflection.foreign_key.to_sym
44
43
  symbolized_column_names = model_klass.column_names.map(&:to_sym)
45
44
 
46
- owner_primary_key = self.owner.class.primary_key
47
- owner_primary_key_value = self.owner.send(owner_primary_key)
45
+ owner_primary_key = owner.class.primary_key
46
+ owner_primary_key_value = owner.send(owner_primary_key)
48
47
 
49
48
  # assume array of model objects
50
- if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
49
+ if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
51
50
  if args.length == 2
52
51
  models = args.last
53
52
  column_names = args.first
@@ -56,54 +55,53 @@ class ActiveRecord::Associations::CollectionAssociation
56
55
  column_names = symbolized_column_names
57
56
  end
58
57
 
59
- if !symbolized_column_names.include?(symbolized_foreign_key)
58
+ unless symbolized_column_names.include?(symbolized_foreign_key)
60
59
  column_names << symbolized_foreign_key
61
60
  end
62
61
 
63
62
  models.each do |m|
64
- m.send "#{symbolized_foreign_key}=", owner_primary_key_value
63
+ m.public_send "#{symbolized_foreign_key}=", owner_primary_key_value
65
64
  end
66
65
 
67
66
  return model_klass.import column_names, models, options
68
67
 
69
68
  # supports empty array
70
- elsif args.last.is_a?( Array ) and args.last.empty?
69
+ elsif args.last.is_a?( Array ) && args.last.empty?
71
70
  return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
72
71
 
73
72
  # supports 2-element array and array
74
- elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
73
+ elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
75
74
  column_names, array_of_attributes = args
76
75
  symbolized_column_names = column_names.map(&:to_s)
77
76
 
78
- if !symbolized_column_names.include?(symbolized_foreign_key)
79
- column_names << symbolized_foreign_key
80
- array_of_attributes.each { |attrs| attrs << owner_primary_key_value }
81
- else
77
+ if symbolized_column_names.include?(symbolized_foreign_key)
82
78
  index = symbolized_column_names.index(symbolized_foreign_key)
83
79
  array_of_attributes.each { |attrs| attrs[index] = owner_primary_key_value }
80
+ else
81
+ column_names << symbolized_foreign_key
82
+ array_of_attributes.each { |attrs| attrs << owner_primary_key_value }
84
83
  end
85
84
 
86
85
  return model_klass.import column_names, array_of_attributes, options
87
86
  else
88
- raise ArgumentError.new( "Invalid arguments!" )
87
+ raise ArgumentError, "Invalid arguments!"
89
88
  end
90
89
  end
91
90
  end
92
91
 
93
92
  class ActiveRecord::Base
94
93
  class << self
95
-
96
94
  # use tz as set in ActiveRecord::Base
97
95
  tproc = lambda do
98
96
  ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
99
97
  end
100
98
 
101
99
  AREXT_RAILS_COLUMNS = {
102
- :create => { "created_on" => tproc ,
103
- "created_at" => tproc },
104
- :update => { "updated_on" => tproc ,
105
- "updated_at" => tproc }
106
- }
100
+ create: { "created_on" => tproc,
101
+ "created_at" => tproc },
102
+ update: { "updated_on" => tproc,
103
+ "updated_at" => tproc }
104
+ }.freeze
107
105
  AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
108
106
 
109
107
  # Returns true if the current database connection adapter
@@ -179,17 +177,19 @@ class ActiveRecord::Base
179
177
  # existing model instances in memory with updates from the import.
180
178
  # * +timestamps+ - true|false, tells import to not add timestamps
181
179
  # (if false) even if record timestamps is disabled in ActiveRecord::Base
182
- # * +recursive - true|false, tells import to import all has_many/has_one
180
+ # * +recursive+ - true|false, tells import to import all has_many/has_one
183
181
  # associations if the adapter supports setting the primary keys of the
184
182
  # newly imported objects.
183
+ # * +batch_size+ - an integer value to specify the max number of records to
184
+ # include per insert. Defaults to the total number of records to import.
185
185
  #
186
186
  # == Examples
187
187
  # class BlogPost < ActiveRecord::Base ; end
188
188
  #
189
189
  # # Example using array of model objects
190
- # posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
191
- # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
192
- # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
190
+ # posts = [ BlogPost.new author_name: 'Zach Dennis', title: 'AREXT',
191
+ # BlogPost.new author_name: 'Zach Dennis', title: 'AREXT2',
192
+ # BlogPost.new author_name: 'Zach Dennis', title: 'AREXT3' ]
193
193
  # BlogPost.import posts
194
194
  #
195
195
  # # Example using column_names and array_of_values
@@ -200,19 +200,19 @@ class ActiveRecord::Base
200
200
  # # Example using column_names, array_of_value and options
201
201
  # columns = [ :author_name, :title ]
202
202
  # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
203
- # BlogPost.import( columns, values, :validate => false )
203
+ # BlogPost.import( columns, values, validate: false )
204
204
  #
205
205
  # # Example synchronizing existing instances in memory
206
206
  # post = BlogPost.where(author_name: 'zdennis').first
207
207
  # puts post.author_name # => 'zdennis'
208
208
  # columns = [ :author_name, :title ]
209
209
  # values = [ [ 'yoda', 'test post' ] ]
210
- # BlogPost.import posts, :synchronize=>[ post ]
210
+ # BlogPost.import posts, synchronize: [ post ]
211
211
  # puts post.author_name # => 'yoda'
212
212
  #
213
213
  # # Example synchronizing unsaved/new instances in memory by using a uniqued imported field
214
- # posts = [BlogPost.new(:title => "Foo"), BlogPost.new(:title => "Bar")]
215
- # BlogPost.import posts, :synchronize => posts, :synchronize_keys => [:title]
214
+ # posts = [BlogPost.new(title: "Foo"), BlogPost.new(title: "Bar")]
215
+ # BlogPost.import posts, synchronize: posts, synchronize_keys: [:title]
216
216
  # puts posts.first.persisted? # => true
217
217
  #
218
218
  # == On Duplicate Key Update (MySQL)
@@ -225,7 +225,7 @@ class ActiveRecord::Base
225
225
  # names. The column names are the only fields that are updated if
226
226
  # a duplicate record is found. Below is an example:
227
227
  #
228
- # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
228
+ # BlogPost.import columns, values, on_duplicate_key_update: [ :date_modified, :content, :author ]
229
229
  #
230
230
  # ==== Using A Hash
231
231
  #
@@ -234,7 +234,7 @@ class ActiveRecord::Base
234
234
  # control over what fields are updated with what attributes on your
235
235
  # model. Below is an example:
236
236
  #
237
- # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
237
+ # BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
238
238
  #
239
239
  # == On Duplicate Key Update (Postgres 9.5+)
240
240
  #
@@ -249,7 +249,7 @@ class ActiveRecord::Base
249
249
  # not work. The column names are the only fields that are updated
250
250
  # if a duplicate record is found. Below is an example:
251
251
  #
252
- # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
252
+ # BlogPost.import columns, values, on_duplicate_key_update: [ :date_modified, :content, :author ]
253
253
  #
254
254
  # ==== Using a Hash
255
255
  #
@@ -266,7 +266,7 @@ class ActiveRecord::Base
266
266
  # but it is the preferred method of identifying a constraint. It will
267
267
  # default to the primary key. Below is an example:
268
268
  #
269
- # BlogPost.import columns, values, :on_duplicate_key_update=>{ :conflict_target => [:author_id, :slug], :columns => [ :date_modified ] }
269
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [:author_id, :slug], columns: [ :date_modified ] }
270
270
  #
271
271
  # ====== :constraint_name
272
272
  #
@@ -274,7 +274,7 @@ class ActiveRecord::Base
274
274
  # unique index by name. Postgres documentation discourages using this method
275
275
  # of identifying an index unless absolutely necessary. Below is an example:
276
276
  #
277
- # BlogPost.import columns, values, :on_duplicate_key_update=>{ :constraint_name => :blog_posts_pkey, :columns => [ :date_modified ] }
277
+ # BlogPost.import columns, values, on_duplicate_key_update: { constraint_name: :blog_posts_pkey, columns: [ :date_modified ] }
278
278
  #
279
279
  # ====== :columns
280
280
  #
@@ -286,7 +286,7 @@ class ActiveRecord::Base
286
286
  # are the only fields that are updated if a duplicate record is found.
287
287
  # Below is an example:
288
288
  #
289
- # BlogPost.import columns, values, :on_duplicate_key_update=>{ :conflict_target => :slug, :columns => [ :date_modified, :content, :author ] }
289
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: [ :date_modified, :content, :author ] }
290
290
  #
291
291
  # ======== Using a Hash
292
292
  #
@@ -294,7 +294,7 @@ class ActiveRecord::Base
294
294
  # mappings. This gives you finer grained control over what fields are updated
295
295
  # with what attributes on your model. Below is an example:
296
296
  #
297
- # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :conflict_target => :slug, :columns => { :title => :title } }
297
+ # BlogPost.import columns, attributes, on_duplicate_key_update: { conflict_target: :slug, columns: { title: :title } }
298
298
  #
299
299
  # = Returns
300
300
  # This returns an object which responds to +failed_instances+ and +num_inserts+.
@@ -302,7 +302,7 @@ class ActiveRecord::Base
302
302
  # * num_inserts - the number of insert statements it took to import the data
303
303
  # * ids - the primary keys of the imported ids, if the adpater supports it, otherwise and empty array.
304
304
  def import(*args)
305
- if args.first.is_a?( Array ) and args.first.first.is_a? ActiveRecord::Base
305
+ if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
306
306
  options = {}
307
307
  options.merge!( args.pop ) if args.last.is_a?(Hash)
308
308
 
@@ -313,8 +313,19 @@ class ActiveRecord::Base
313
313
  end
314
314
  end
315
315
 
316
+ # Imports a collection of values if all values are valid. Import fails at the
317
+ # first encountered validation error and raises ActiveRecord::RecordInvalid
318
+ # with the failed instance.
319
+ def import!(*args)
320
+ options = args.last.is_a?( Hash ) ? args.pop : {}
321
+ options[:validate] = true
322
+ options[:raise_error] = true
323
+
324
+ import(*args, options)
325
+ end
326
+
316
327
  def import_helper( *args )
317
- options = { :validate=>true, :timestamps=>true, :primary_key=>primary_key }
328
+ options = { validate: true, timestamps: true, primary_key: primary_key }
318
329
  options.merge!( args.pop ) if args.last.is_a? Hash
319
330
 
320
331
  # Don't modify incoming arguments
@@ -326,7 +337,7 @@ class ActiveRecord::Base
326
337
  is_validating = true unless options[:validate_with_context].nil?
327
338
 
328
339
  # assume array of model objects
329
- if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
340
+ if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
330
341
  if args.length == 2
331
342
  models = args.last
332
343
  column_names = args.first
@@ -338,26 +349,24 @@ class ActiveRecord::Base
338
349
  array_of_attributes = models.map do |model|
339
350
  # this next line breaks sqlite.so with a segmentation fault
340
351
  # if model.new_record? || options[:on_duplicate_key_update]
341
- column_names.map do |name|
342
- name = name.to_s
343
- if respond_to?(:defined_enums) && defined_enums.has_key?(name) # ActiveRecord 5
344
- model.read_attribute(name)
345
- elsif model.class.column_defaults[name].is_a?(Integer)
346
- model.read_attribute(name)
347
- else
348
- model.read_attribute_before_type_cast(name)
349
- end
352
+ column_names.map do |name|
353
+ name = name.to_s
354
+ if respond_to?(:defined_enums) && defined_enums.key?(name) # ActiveRecord 5
355
+ model.read_attribute(name)
356
+ else
357
+ model.read_attribute_before_type_cast(name)
350
358
  end
359
+ end
351
360
  # end
352
361
  end
353
362
  # supports empty array
354
- elsif args.last.is_a?( Array ) and args.last.empty?
363
+ elsif args.last.is_a?( Array ) && args.last.empty?
355
364
  return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
356
365
  # supports 2-element array and array
357
- elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
366
+ elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
358
367
  column_names, array_of_attributes = args
359
368
  else
360
- raise ArgumentError.new( "Invalid arguments!" )
369
+ raise ArgumentError, "Invalid arguments!"
361
370
  end
362
371
 
363
372
  # dup the passed in array so we don't modify it unintentionally
@@ -368,13 +377,13 @@ class ActiveRecord::Base
368
377
  # on the list and we are using a sequence and stuff a nil
369
378
  # value for it into each row so the sequencer will fire later
370
379
  if !column_names.include?(primary_key) && connection.prefetch_primary_key? && sequence_name
371
- column_names << primary_key
372
- array_of_attributes.each { |a| a << nil }
380
+ column_names << primary_key
381
+ array_of_attributes.each { |a| a << nil }
373
382
  end
374
383
 
375
384
  # record timestamps unless disabled in ActiveRecord::Base
376
385
  if record_timestamps && options.delete( :timestamps )
377
- add_special_rails_stamps column_names, array_of_attributes, options
386
+ add_special_rails_stamps column_names, array_of_attributes, options
378
387
  end
379
388
 
380
389
  return_obj = if is_validating
@@ -385,7 +394,7 @@ class ActiveRecord::Base
385
394
  end
386
395
 
387
396
  if options[:synchronize]
388
- sync_keys = options[:synchronize_keys] || [self.primary_key]
397
+ sync_keys = options[:synchronize_keys] || [primary_key]
389
398
  synchronize( options[:synchronize], sync_keys)
390
399
  end
391
400
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
@@ -395,9 +404,7 @@ class ActiveRecord::Base
395
404
  set_ids_and_mark_clean(models, return_obj)
396
405
 
397
406
  # if there are auto-save associations on the models we imported that are new, import them as well
398
- if options[:recursive]
399
- import_associations(models, options)
400
- end
407
+ import_associations(models, options.dup) if options[:recursive]
401
408
  end
402
409
 
403
410
  return_obj
@@ -414,7 +421,7 @@ class ActiveRecord::Base
414
421
  # +num_inserts+ is the number of inserts it took to import the data. See
415
422
  # ActiveRecord::Base.import for more information on
416
423
  # +column_names+, +array_of_attributes+ and +options+.
417
- def import_with_validations( column_names, array_of_attributes, options={} )
424
+ def import_with_validations( column_names, array_of_attributes, options = {} )
418
425
  failed_instances = []
419
426
 
420
427
  # create instances for each of our column/value sets
@@ -422,23 +429,23 @@ class ActiveRecord::Base
422
429
 
423
430
  # keep track of the instance and the position it is currently at. if this fails
424
431
  # validation we'll use the index to remove it from the array_of_attributes
425
- arr.each_with_index do |hsh,i|
432
+ arr.each_with_index do |hsh, i|
426
433
  instance = new do |model|
427
- hsh.each_pair{ |k,v| model.send("#{k}=", v) }
434
+ hsh.each_pair { |k, v| model[k] = v }
428
435
  end
429
436
 
430
- if not instance.valid?(options[:validate_with_context])
431
- array_of_attributes[ i ] = nil
432
- failed_instances << instance
433
- end
437
+ next if instance.valid?(options[:validate_with_context])
438
+ raise(ActiveRecord::RecordInvalid, instance) if options[:raise_error]
439
+ array_of_attributes[i] = nil
440
+ failed_instances << instance
434
441
  end
435
442
  array_of_attributes.compact!
436
443
 
437
- (num_inserts, ids) = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
438
- [0,[]]
439
- else
440
- import_without_validations_or_callbacks( column_names, array_of_attributes, options )
441
- end
444
+ num_inserts, ids = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
445
+ [0, []]
446
+ else
447
+ import_without_validations_or_callbacks( column_names, array_of_attributes, options )
448
+ end
442
449
  ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids)
443
450
  end
444
451
 
@@ -448,7 +455,7 @@ class ActiveRecord::Base
448
455
  # validations or callbacks. See ActiveRecord::Base.import for more
449
456
  # information on +column_names+, +array_of_attributes_ and
450
457
  # +options+.
451
- def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
458
+ def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
452
459
  column_names = column_names.map(&:to_sym)
453
460
  scope_columns, scope_values = scope_attributes.to_a.transpose
454
461
 
@@ -473,24 +480,30 @@ class ActiveRecord::Base
473
480
  column
474
481
  end
475
482
 
476
- columns_sql = "(#{column_names.map{|name| connection.quote_column_name(name) }.join(',')})"
477
- insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{quoted_table_name} #{columns_sql} VALUES "
483
+ columns_sql = "(#{column_names.map { |name| connection.quote_column_name(name) }.join(',')})"
484
+ insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ' : ''}INTO #{quoted_table_name} #{columns_sql} VALUES "
478
485
  values_sql = values_sql_for_columns_and_attributes(columns, array_of_attributes)
486
+
487
+ number_inserted = 0
479
488
  ids = []
480
- if not supports_import?
481
- number_inserted = 0
489
+ if supports_import?
490
+ # generate the sql
491
+ post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
492
+
493
+ batch_size = options[:batch_size] || values_sql.size
494
+ values_sql.each_slice(batch_size) do |batch_values|
495
+ # perform the inserts
496
+ result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
497
+ batch_values,
498
+ "#{self.class.name} Create Many Without Validations Or Callbacks" )
499
+ number_inserted += result[0]
500
+ ids += result[1]
501
+ end
502
+ else
482
503
  values_sql.each do |values|
483
504
  connection.execute(insert_sql + values)
484
505
  number_inserted += 1
485
506
  end
486
- else
487
- # generate the sql
488
- post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
489
-
490
- # perform the inserts
491
- (number_inserted,ids) = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
492
- values_sql,
493
- "#{self.class.name} Create Many Without Validations Or Callbacks" )
494
507
  end
495
508
  [number_inserted, ids]
496
509
  end
@@ -498,17 +511,16 @@ class ActiveRecord::Base
498
511
  private
499
512
 
500
513
  def set_ids_and_mark_clean(models, import_result)
501
- unless models.nil?
502
- import_result.ids.each_with_index do |id, index|
503
- model = models[index]
504
- model.id = id.to_i
505
- if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
506
- model.clear_changes_information
507
- else # Rails 3.1
508
- model.instance_variable_get(:@changed_attributes).clear
509
- end
510
- model.instance_variable_set(:@new_record, false)
514
+ return if models.nil?
515
+ import_result.ids.each_with_index do |id, index|
516
+ model = models[index]
517
+ model.id = id.to_i
518
+ if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
519
+ model.clear_changes_information
520
+ else # Rails 3.1
521
+ model.instance_variable_get(:@changed_attributes).clear
511
522
  end
523
+ model.instance_variable_set(:@new_record, false)
512
524
  end
513
525
  end
514
526
 
@@ -517,11 +529,14 @@ class ActiveRecord::Base
517
529
  # notes:
518
530
  # does not handle associations that reference themselves
519
531
  # should probably take a hash to associations to follow.
520
- associated_objects_by_class={}
521
- models.each {|model| find_associated_objects_for_import(associated_objects_by_class, model) }
532
+ associated_objects_by_class = {}
533
+ models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
534
+
535
+ # :on_duplicate_key_update not supported for associations
536
+ options.delete(:on_duplicate_key_update)
522
537
 
523
- associated_objects_by_class.each_pair do |class_name, associations|
524
- associations.each_pair do |association_name, associated_records|
538
+ associated_objects_by_class.each_value do |associations|
539
+ associations.each_value do |associated_records|
525
540
  associated_records.first.class.import(associated_records, options) unless associated_records.empty?
526
541
  end
527
542
  end
@@ -530,13 +545,13 @@ class ActiveRecord::Base
530
545
  # We are eventually going to call Class.import <objects> so we build up a hash
531
546
  # of class => objects to import.
532
547
  def find_associated_objects_for_import(associated_objects_by_class, model)
533
- associated_objects_by_class[model.class.name]||={}
548
+ associated_objects_by_class[model.class.name] ||= {}
534
549
 
535
550
  association_reflections =
536
551
  model.class.reflect_on_all_associations(:has_one) +
537
552
  model.class.reflect_on_all_associations(:has_many)
538
553
  association_reflections.each do |association_reflection|
539
- associated_objects_by_class[model.class.name][association_reflection.name]||=[]
554
+ associated_objects_by_class[model.class.name][association_reflection.name] ||= []
540
555
 
541
556
  association = model.association(association_reflection.name)
542
557
  association.loaded!
@@ -544,9 +559,13 @@ class ActiveRecord::Base
544
559
  # Wrap target in an array if not already
545
560
  association = Array(association.target)
546
561
 
547
- changed_objects = association.select {|a| a.new_record? || a.changed?}
562
+ changed_objects = association.select { |a| a.new_record? || a.changed? }
548
563
  changed_objects.each do |child|
549
- child.send("#{association_reflection.foreign_key}=", model.id)
564
+ child.public_send("#{association_reflection.foreign_key}=", model.id)
565
+ # For polymorphic associations
566
+ association_reflection.type.try do |type|
567
+ child.public_send("#{type}=", model.class.name)
568
+ end
550
569
  end
551
570
  associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
552
571
  end
@@ -555,23 +574,26 @@ class ActiveRecord::Base
555
574
 
556
575
  # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
557
576
  # and +array_of_attributes+.
558
- def values_sql_for_columns_and_attributes(columns, array_of_attributes) # :nodoc:
577
+ def values_sql_for_columns_and_attributes(columns, array_of_attributes) # :nodoc:
559
578
  # connection gets called a *lot* in this high intensity loop.
560
579
  # Reuse the same one w/in the loop, otherwise it would keep being re-retreived (= lots of time for large imports)
561
580
  connection_memo = connection
562
581
  array_of_attributes.map do |arr|
563
- my_values = arr.each_with_index.map do |val,j|
582
+ my_values = arr.each_with_index.map do |val, j|
564
583
  column = columns[j]
565
584
 
566
585
  # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
567
586
  if val.nil? && column.name == primary_key && !sequence_name.blank?
568
- connection_memo.next_value_for_sequence(sequence_name)
587
+ connection_memo.next_value_for_sequence(sequence_name)
569
588
  elsif column
570
589
  if respond_to?(:type_caster) && type_caster.respond_to?(:type_cast_for_database) # Rails 5.0 and higher
571
590
  connection_memo.quote(type_caster.type_cast_for_database(column.name, val))
572
- elsif column.respond_to?(:type_cast_from_user) # Rails 4.2 and higher
591
+ elsif column.respond_to?(:type_cast_from_user) # Rails 4.2 and higher
573
592
  connection_memo.quote(column.type_cast_from_user(val), column)
574
- else # Rails 3.1, 3.2, and 4.1
593
+ else # Rails 3.1, 3.2, 4.0 and 4.1
594
+ if serialized_attributes.include?(column.name)
595
+ val = serialized_attributes[column.name].dump(val)
596
+ end
575
597
  connection_memo.quote(column.type_cast(val), column)
576
598
  end
577
599
  end
@@ -582,32 +604,32 @@ class ActiveRecord::Base
582
604
 
583
605
  def add_special_rails_stamps( column_names, array_of_attributes, options )
584
606
  AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
585
- if self.column_names.include?(key)
586
- value = blk.call
587
- if index=column_names.index(key) || index=column_names.index(key.to_sym)
588
- # replace every instance of the array of attributes with our value
589
- array_of_attributes.each{ |arr| arr[index] = value if arr[index].nil? }
590
- else
591
- column_names << key
592
- array_of_attributes.each { |arr| arr << value }
593
- end
607
+ next unless self.column_names.include?(key)
608
+ value = blk.call
609
+ index = column_names.index(key) || column_names.index(key.to_sym)
610
+ if index
611
+ # replace every instance of the array of attributes with our value
612
+ array_of_attributes.each { |arr| arr[index] = value if arr[index].nil? }
613
+ else
614
+ column_names << key
615
+ array_of_attributes.each { |arr| arr << value }
594
616
  end
595
617
  end
596
618
 
597
619
  AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
598
- if self.column_names.include?(key)
599
- value = blk.call
600
- if index=column_names.index(key) || index=column_names.index(key.to_sym)
601
- # replace every instance of the array of attributes with our value
602
- array_of_attributes.each{ |arr| arr[index] = value }
603
- else
604
- column_names << key
605
- array_of_attributes.each { |arr| arr << value }
606
- end
620
+ next unless self.column_names.include?(key)
621
+ value = blk.call
622
+ index = column_names.index(key) || column_names.index(key.to_sym)
623
+ if index
624
+ # replace every instance of the array of attributes with our value
625
+ array_of_attributes.each { |arr| arr[index] = value }
626
+ else
627
+ column_names << key
628
+ array_of_attributes.each { |arr| arr << value }
629
+ end
607
630
 
608
- if supports_on_duplicate_key_update?
609
- connection.add_column_for_on_duplicate_key_update(key, options)
610
- end
631
+ if supports_on_duplicate_key_update?
632
+ connection.add_column_for_on_duplicate_key_update(key, options)
611
633
  end
612
634
  end
613
635
  end
@@ -615,9 +637,8 @@ class ActiveRecord::Base
615
637
  # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
616
638
  def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
617
639
  array_of_attributes.map do |attributes|
618
- Hash[attributes.each_with_index.map {|attr, c| [column_names[c], attr] }]
640
+ Hash[attributes.each_with_index.map { |attr, c| [column_names[c], attr] }]
619
641
  end
620
642
  end
621
-
622
643
  end
623
644
  end