activerecord-import 0.12.0 → 0.13.0

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