activerecord-import 0.19.0 → 1.0.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 (43) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -12
  3. data/CHANGELOG.md +166 -0
  4. data/Gemfile +13 -10
  5. data/README.markdown +548 -5
  6. data/Rakefile +2 -1
  7. data/benchmarks/lib/cli_parser.rb +2 -1
  8. data/gemfiles/5.1.gemfile +1 -0
  9. data/gemfiles/5.2.gemfile +2 -0
  10. data/lib/activerecord-import/adapters/abstract_adapter.rb +2 -2
  11. data/lib/activerecord-import/adapters/mysql_adapter.rb +16 -10
  12. data/lib/activerecord-import/adapters/postgresql_adapter.rb +59 -15
  13. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +126 -3
  14. data/lib/activerecord-import/base.rb +4 -6
  15. data/lib/activerecord-import/import.rb +384 -126
  16. data/lib/activerecord-import/synchronize.rb +1 -1
  17. data/lib/activerecord-import/value_sets_parser.rb +14 -0
  18. data/lib/activerecord-import/version.rb +1 -1
  19. data/lib/activerecord-import.rb +2 -15
  20. data/test/adapters/makara_postgis.rb +1 -0
  21. data/test/import_test.rb +148 -14
  22. data/test/makara_postgis/import_test.rb +8 -0
  23. data/test/models/account.rb +3 -0
  24. data/test/models/bike_maker.rb +7 -0
  25. data/test/models/topic.rb +10 -0
  26. data/test/models/user.rb +3 -0
  27. data/test/models/user_token.rb +4 -0
  28. data/test/schema/generic_schema.rb +20 -0
  29. data/test/schema/mysql2_schema.rb +19 -0
  30. data/test/schema/postgresql_schema.rb +1 -0
  31. data/test/schema/sqlite3_schema.rb +13 -0
  32. data/test/support/factories.rb +9 -8
  33. data/test/support/generate.rb +6 -6
  34. data/test/support/mysql/import_examples.rb +14 -2
  35. data/test/support/postgresql/import_examples.rb +142 -0
  36. data/test/support/shared_examples/on_duplicate_key_update.rb +252 -1
  37. data/test/support/shared_examples/recursive_import.rb +41 -11
  38. data/test/support/sqlite3/import_examples.rb +187 -10
  39. data/test/synchronize_test.rb +8 -0
  40. data/test/test_helper.rb +9 -1
  41. data/test/value_sets_bytes_parser_test.rb +13 -2
  42. metadata +20 -5
  43. data/test/schema/mysql_schema.rb +0 -16
@@ -3,7 +3,7 @@ require "ostruct"
3
3
  module ActiveRecord::Import::ConnectionAdapters; end
4
4
 
5
5
  module ActiveRecord::Import #:nodoc:
6
- Result = Struct.new(:failed_instances, :num_inserts, :ids)
6
+ Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
7
7
 
8
8
  module ImportSupport #:nodoc:
9
9
  def supports_import? #:nodoc:
@@ -24,27 +24,63 @@ module ActiveRecord::Import #:nodoc:
24
24
  end
25
25
 
26
26
  class Validator
27
- def initialize(options = {})
27
+ def initialize(klass, options = {})
28
28
  @options = options
29
+ init_validations(klass)
30
+ end
31
+
32
+ def init_validations(klass)
33
+ @validate_callbacks = klass._validate_callbacks.dup
34
+
35
+ @validate_callbacks.each_with_index do |callback, i|
36
+ filter = callback.raw_filter
37
+ next unless filter.class.name =~ /Validations::PresenceValidator/ ||
38
+ (!@options[:validate_uniqueness] &&
39
+ filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
40
+
41
+ callback = callback.dup
42
+ filter = filter.dup
43
+ attrs = filter.instance_variable_get(:@attributes).dup
44
+
45
+ if filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
46
+ attrs = []
47
+ else
48
+ associations = klass.reflect_on_all_associations(:belongs_to)
49
+ associations.each do |assoc|
50
+ if (index = attrs.index(assoc.name))
51
+ key = assoc.foreign_key.to_sym
52
+ attrs[index] = key unless attrs.include?(key)
53
+ end
54
+ end
55
+ end
56
+
57
+ filter.instance_variable_set(:@attributes, attrs)
58
+
59
+ if @validate_callbacks.respond_to?(:chain, true)
60
+ @validate_callbacks.send(:chain).tap do |chain|
61
+ callback.instance_variable_set(:@filter, filter)
62
+ chain[i] = callback
63
+ end
64
+ else
65
+ callback.raw_filter = filter
66
+ callback.filter = callback.send(:_compile_filter, filter)
67
+ @validate_callbacks[i] = callback
68
+ end
69
+ end
29
70
  end
30
71
 
31
72
  def valid_model?(model)
32
73
  validation_context = @options[:validate_with_context]
33
74
  validation_context ||= (model.new_record? ? :create : :update)
34
-
35
75
  current_context = model.send(:validation_context)
76
+
36
77
  begin
37
78
  model.send(:validation_context=, validation_context)
38
79
  model.errors.clear
39
80
 
40
- validate_callbacks = model._validate_callbacks.dup
41
- validate_callbacks.each do |callback|
42
- validate_callbacks.delete(callback) if callback.raw_filter.is_a? ActiveRecord::Validations::UniquenessValidator
43
- end
44
-
45
81
  model.run_callbacks(:validation) do
46
82
  if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
47
- runner = validate_callbacks.compile
83
+ runner = @validate_callbacks.compile
48
84
  env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
49
85
  if runner.respond_to?(:call) # ActiveRecord < 5.1
50
86
  runner.call(env)
@@ -63,10 +99,10 @@ module ActiveRecord::Import #:nodoc:
63
99
  runner.invoke_before(env)
64
100
  runner.invoke_after(env)
65
101
  end
66
- elsif validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
67
- model.instance_eval validate_callbacks.compile
102
+ elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
103
+ model.instance_eval @validate_callbacks.compile
68
104
  else # ActiveRecord 3.x
69
- model.instance_eval validate_callbacks.compile(nil, model)
105
+ model.instance_eval @validate_callbacks.compile(nil, model)
70
106
  end
71
107
  end
72
108
 
@@ -79,13 +115,14 @@ module ActiveRecord::Import #:nodoc:
79
115
  end
80
116
 
81
117
  class ActiveRecord::Associations::CollectionProxy
82
- def import(*args, &block)
83
- @association.import(*args, &block)
118
+ def bulk_import(*args, &block)
119
+ @association.bulk_import(*args, &block)
84
120
  end
121
+ alias import bulk_import unless respond_to? :import
85
122
  end
86
123
 
87
124
  class ActiveRecord::Associations::CollectionAssociation
88
- def import(*args, &block)
125
+ def bulk_import(*args, &block)
89
126
  unless owner.persisted?
90
127
  raise ActiveRecord::RecordNotSaved, "You cannot call import unless the parent is saved"
91
128
  end
@@ -94,16 +131,21 @@ class ActiveRecord::Associations::CollectionAssociation
94
131
 
95
132
  model_klass = reflection.klass
96
133
  symbolized_foreign_key = reflection.foreign_key.to_sym
97
- symbolized_column_names = model_klass.column_names.map(&:to_sym)
98
134
 
99
- owner_primary_key = owner.class.primary_key
135
+ symbolized_column_names = if model_klass.connection.respond_to?(:supports_virtual_columns?) && model_klass.connection.supports_virtual_columns?
136
+ model_klass.columns.reject(&:virtual?).map { |c| c.name.to_sym }
137
+ else
138
+ model_klass.column_names.map(&:to_sym)
139
+ end
140
+
141
+ owner_primary_key = reflection.active_record_primary_key.to_sym
100
142
  owner_primary_key_value = owner.send(owner_primary_key)
101
143
 
102
144
  # assume array of model objects
103
145
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
104
146
  if args.length == 2
105
147
  models = args.last
106
- column_names = args.first
148
+ column_names = args.first.dup
107
149
  else
108
150
  models = args.first
109
151
  column_names = symbolized_column_names
@@ -118,7 +160,46 @@ class ActiveRecord::Associations::CollectionAssociation
118
160
  m.public_send "#{reflection.type}=", owner.class.name if reflection.type
119
161
  end
120
162
 
121
- return model_klass.import column_names, models, options
163
+ return model_klass.bulk_import column_names, models, options
164
+
165
+ # supports array of hash objects
166
+ elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
167
+ if args.length == 2
168
+ array_of_hashes = args.last
169
+ column_names = args.first.dup
170
+ allow_extra_hash_keys = true
171
+ else
172
+ array_of_hashes = args.first
173
+ column_names = array_of_hashes.first.keys
174
+ allow_extra_hash_keys = false
175
+ end
176
+
177
+ symbolized_column_names = column_names.map(&:to_sym)
178
+ unless symbolized_column_names.include?(symbolized_foreign_key)
179
+ column_names << symbolized_foreign_key
180
+ end
181
+
182
+ if reflection.type && !symbolized_column_names.include?(reflection.type.to_sym)
183
+ column_names << reflection.type.to_sym
184
+ end
185
+
186
+ array_of_attributes = array_of_hashes.map do |h|
187
+ error_message = model_klass.send(:validate_hash_import, h, symbolized_column_names, allow_extra_hash_keys)
188
+
189
+ raise ArgumentError, error_message if error_message
190
+
191
+ column_names.map do |key|
192
+ if key == symbolized_foreign_key
193
+ owner_primary_key_value
194
+ elsif reflection.type && key == reflection.type.to_sym
195
+ owner.class.name
196
+ else
197
+ h[key]
198
+ end
199
+ end
200
+ end
201
+
202
+ return model_klass.bulk_import column_names, array_of_attributes, options
122
203
 
123
204
  # supports empty array
124
205
  elsif args.last.is_a?( Array ) && args.last.empty?
@@ -127,7 +208,12 @@ class ActiveRecord::Associations::CollectionAssociation
127
208
  # supports 2-element array and array
128
209
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
129
210
  column_names, array_of_attributes = args
130
- symbolized_column_names = column_names.map(&:to_s)
211
+
212
+ # dup the passed args so we don't modify unintentionally
213
+ column_names = column_names.dup
214
+ array_of_attributes = array_of_attributes.map(&:dup)
215
+
216
+ symbolized_column_names = column_names.map(&:to_sym)
131
217
 
132
218
  if symbolized_column_names.include?(symbolized_foreign_key)
133
219
  index = symbolized_column_names.index(symbolized_foreign_key)
@@ -138,19 +224,35 @@ class ActiveRecord::Associations::CollectionAssociation
138
224
  end
139
225
 
140
226
  if reflection.type
141
- column_names << reflection.type
142
- array_of_attributes.each { |attrs| attrs << owner.class.name }
227
+ symbolized_type = reflection.type.to_sym
228
+ if symbolized_column_names.include?(symbolized_type)
229
+ index = symbolized_column_names.index(symbolized_type)
230
+ array_of_attributes.each { |attrs| attrs[index] = owner.class.name }
231
+ else
232
+ column_names << symbolized_type
233
+ array_of_attributes.each { |attrs| attrs << owner.class.name }
234
+ end
143
235
  end
144
236
 
145
- return model_klass.import column_names, array_of_attributes, options
237
+ return model_klass.bulk_import column_names, array_of_attributes, options
146
238
  else
147
239
  raise ArgumentError, "Invalid arguments!"
148
240
  end
149
241
  end
242
+ alias import bulk_import unless respond_to? :import
150
243
  end
151
244
 
152
245
  class ActiveRecord::Base
153
246
  class << self
247
+ def establish_connection_with_activerecord_import(*args)
248
+ conn = establish_connection_without_activerecord_import(*args)
249
+ ActiveRecord::Import.load_from_connection_pool connection_pool
250
+ conn
251
+ end
252
+
253
+ alias establish_connection_without_activerecord_import establish_connection
254
+ alias establish_connection establish_connection_with_activerecord_import
255
+
154
256
  # Returns true if the current database connection adapter
155
257
  # supports import functionality, otherwise returns false.
156
258
  def supports_import?(*args)
@@ -161,14 +263,14 @@ class ActiveRecord::Base
161
263
  # supports on duplicate key update functionality, otherwise
162
264
  # returns false.
163
265
  def supports_on_duplicate_key_update?
164
- connection.supports_on_duplicate_key_update?
266
+ connection.respond_to?(:supports_on_duplicate_key_update?) && connection.supports_on_duplicate_key_update?
165
267
  end
166
268
 
167
269
  # returns true if the current database connection adapter
168
270
  # supports setting the primary key of bulk imported models, otherwise
169
271
  # returns false
170
- def support_setting_primary_key_of_imported_objects?
171
- connection.respond_to?(:support_setting_primary_key_of_imported_objects?) && connection.support_setting_primary_key_of_imported_objects?
272
+ def supports_setting_primary_key_of_imported_objects?
273
+ connection.respond_to?(:supports_setting_primary_key_of_imported_objects?) && connection.supports_setting_primary_key_of_imported_objects?
172
274
  end
173
275
 
174
276
  # Imports a collection of values to the database.
@@ -214,18 +316,21 @@ class ActiveRecord::Base
214
316
  #
215
317
  # == Options
216
318
  # * +validate+ - true|false, tells import whether or not to use
217
- # ActiveRecord validations. Validations are enforced by default.
319
+ # ActiveRecord validations. Validations are enforced by default.
320
+ # It skips the uniqueness validation for performance reasons.
321
+ # You can find more details here:
322
+ # https://github.com/zdennis/activerecord-import/issues/228
218
323
  # * +ignore+ - true|false, an alias for on_duplicate_key_ignore.
219
324
  # * +on_duplicate_key_ignore+ - true|false, tells import to discard
220
- # records that contain duplicate keys. For Postgres 9.5+ it adds
221
- # ON CONFLICT DO NOTHING, for MySQL it uses INSERT IGNORE, and for
222
- # SQLite it uses INSERT OR IGNORE. Cannot be enabled on a
223
- # recursive import. For database adapters that normally support
224
- # setting primary keys on imported objects, this option prevents
225
- # that from occurring.
226
- # * +on_duplicate_key_update+ - an Array or Hash, tells import to
227
- # use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
228
- # DO UPDATE ability. See On Duplicate Key Update below.
325
+ # records that contain duplicate keys. For Postgres 9.5+ it adds
326
+ # ON CONFLICT DO NOTHING, for MySQL it uses INSERT IGNORE, and for
327
+ # SQLite it uses INSERT OR IGNORE. Cannot be enabled on a
328
+ # recursive import. For database adapters that normally support
329
+ # setting primary keys on imported objects, this option prevents
330
+ # that from occurring.
331
+ # * +on_duplicate_key_update+ - :all, an Array, or Hash, tells import to
332
+ # use MySQL's ON DUPLICATE KEY UPDATE or Postgres/SQLite ON CONFLICT
333
+ # DO UPDATE ability. See On Duplicate Key Update below.
229
334
  # * +synchronize+ - an array of ActiveRecord instances for the model
230
335
  # that you are currently importing data into. This synchronizes
231
336
  # existing model instances in memory with updates from the import.
@@ -247,6 +352,9 @@ class ActiveRecord::Base
247
352
  # BlogPost.import posts
248
353
  #
249
354
  # # Example using array_of_hash_objects
355
+ # # NOTE: column_names will be determined by using the keys of the first hash in the array. If later hashes in the
356
+ # # array have different keys an exception will be raised. If you have hashes to import with different sets of keys
357
+ # # we recommend grouping these into batches before importing.
250
358
  # values = [ {author_name: 'zdennis', title: 'test post'} ], [ {author_name: 'jdoe', title: 'another test post'} ] ]
251
359
  # BlogPost.import values
252
360
  #
@@ -280,7 +388,15 @@ class ActiveRecord::Base
280
388
  #
281
389
  # == On Duplicate Key Update (MySQL)
282
390
  #
283
- # The :on_duplicate_key_update option can be either an Array or a Hash.
391
+ # The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
392
+ #
393
+ # ==== Using :all
394
+ #
395
+ # The :on_duplicate_key_update option can be set to :all. All columns
396
+ # other than the primary key are updated. If a list of column names is
397
+ # supplied, only those columns will be updated. Below is an example:
398
+ #
399
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
284
400
  #
285
401
  # ==== Using an Array
286
402
  #
@@ -299,11 +415,19 @@ class ActiveRecord::Base
299
415
  #
300
416
  # BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
301
417
  #
302
- # == On Duplicate Key Update (Postgres 9.5+)
418
+ # == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
303
419
  #
304
- # The :on_duplicate_key_update option can be an Array or a Hash with up to
420
+ # The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
305
421
  # three attributes, :conflict_target (and optionally :index_predicate) or
306
- # :constraint_name, and :columns.
422
+ # :constraint_name (Postgres), and :columns.
423
+ #
424
+ # ==== Using :all
425
+ #
426
+ # The :on_duplicate_key_update option can be set to :all. All columns
427
+ # other than the primary key are updated. If a list of column names is
428
+ # supplied, only those columns will be updated. Below is an example:
429
+ #
430
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
307
431
  #
308
432
  # ==== Using an Array
309
433
  #
@@ -323,7 +447,7 @@ class ActiveRecord::Base
323
447
  # conflicting constraint to be explicitly specified. Using this option
324
448
  # allows you to specify a constraint other than the primary key.
325
449
  #
326
- # ====== :conflict_target
450
+ # ===== :conflict_target
327
451
  #
328
452
  # The :conflict_target attribute specifies the columns that make up the
329
453
  # conflicting unique constraint and can be a single column or an array of
@@ -333,7 +457,7 @@ class ActiveRecord::Base
333
457
  #
334
458
  # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], columns: [ :date_modified ] }
335
459
  #
336
- # ====== :index_predicate
460
+ # ===== :index_predicate
337
461
  #
338
462
  # The :index_predicate attribute optionally specifies a WHERE condition
339
463
  # on :conflict_target, which is required for matching against partial
@@ -342,7 +466,7 @@ class ActiveRecord::Base
342
466
  #
343
467
  # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], index_predicate: 'status <> 0', columns: [ :date_modified ] }
344
468
  #
345
- # ====== :constraint_name
469
+ # ===== :constraint_name
346
470
  #
347
471
  # The :constraint_name attribute explicitly identifies the conflicting
348
472
  # unique index by name. Postgres documentation discourages using this method
@@ -350,7 +474,7 @@ class ActiveRecord::Base
350
474
  #
351
475
  # BlogPost.import columns, values, on_duplicate_key_update: { constraint_name: :blog_posts_pkey, columns: [ :date_modified ] }
352
476
  #
353
- # ====== :condition
477
+ # ===== :condition
354
478
  #
355
479
  # The :condition attribute optionally specifies a WHERE condition
356
480
  # on :conflict_action. Only rows for which this expression returns true will be updated.
@@ -358,12 +482,20 @@ class ActiveRecord::Base
358
482
  # Below is an example:
359
483
  #
360
484
  # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id ], condition: "blog_posts.title NOT LIKE '%sample%'", columns: [ :author_name ] }
361
-
362
- # ====== :columns
363
485
  #
364
- # The :columns attribute can be either an Array or a Hash.
486
+ # ===== :columns
487
+ #
488
+ # The :columns attribute can be either :all, an Array, or a Hash.
489
+ #
490
+ # ===== Using :all
491
+ #
492
+ # The :columns attribute can be :all. All columns other than the primary key will be updated.
493
+ # If a list of column names is supplied, only those columns will be updated.
494
+ # Below is an example:
495
+ #
496
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
365
497
  #
366
- # ======== Using an Array
498
+ # ===== Using an Array
367
499
  #
368
500
  # The :columns attribute can be an array of column names. The column names
369
501
  # are the only fields that are updated if a duplicate record is found.
@@ -371,7 +503,7 @@ class ActiveRecord::Base
371
503
  #
372
504
  # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: [ :date_modified, :content, :author ] }
373
505
  #
374
- # ======== Using a Hash
506
+ # ===== Using a Hash
375
507
  #
376
508
  # The :columns option can be a hash of column names to model attribute name
377
509
  # mappings. This gives you finer grained control over what fields are updated
@@ -384,7 +516,8 @@ class ActiveRecord::Base
384
516
  # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
385
517
  # * num_inserts - the number of insert statements it took to import the data
386
518
  # * ids - the primary keys of the imported ids if the adapter supports it, otherwise an empty array.
387
- def import(*args)
519
+ # * results - import results if the adapter supports it, otherwise an empty array.
520
+ def bulk_import(*args)
388
521
  if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
389
522
  options = {}
390
523
  options.merge!( args.pop ) if args.last.is_a?(Hash)
@@ -395,31 +528,29 @@ class ActiveRecord::Base
395
528
  import_helper(*args)
396
529
  end
397
530
  end
531
+ alias import bulk_import unless respond_to? :import
398
532
 
399
533
  # Imports a collection of values if all values are valid. Import fails at the
400
534
  # first encountered validation error and raises ActiveRecord::RecordInvalid
401
535
  # with the failed instance.
402
- def import!(*args)
536
+ def bulk_import!(*args)
403
537
  options = args.last.is_a?( Hash ) ? args.pop : {}
404
538
  options[:validate] = true
405
539
  options[:raise_error] = true
406
540
 
407
- import(*args, options)
541
+ bulk_import(*args, options)
408
542
  end
543
+ alias import! bulk_import! unless respond_to? :import!
409
544
 
410
545
  def import_helper( *args )
411
546
  options = { validate: true, timestamps: true }
412
547
  options.merge!( args.pop ) if args.last.is_a? Hash
413
548
  # making sure that current model's primary key is used
414
549
  options[:primary_key] = primary_key
550
+ options[:locking_column] = locking_column if attribute_names.include?(locking_column)
415
551
 
416
- # Don't modify incoming arguments
417
- if options[:on_duplicate_key_update] && options[:on_duplicate_key_update].duplicable?
418
- options[:on_duplicate_key_update] = options[:on_duplicate_key_update].dup
419
- end
420
-
421
- is_validating = options[:validate]
422
- is_validating = true unless options[:validate_with_context].nil?
552
+ is_validating = options[:validate_with_context].present? ? true : options[:validate]
553
+ validator = ActiveRecord::Import::Validator.new(self, options)
423
554
 
424
555
  # assume array of model objects
425
556
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
@@ -428,11 +559,19 @@ class ActiveRecord::Base
428
559
  column_names = args.first.dup
429
560
  else
430
561
  models = args.first
431
- column_names = self.column_names.dup
562
+ column_names = if connection.respond_to?(:supports_virtual_columns?) && connection.supports_virtual_columns?
563
+ columns.reject(&:virtual?).map(&:name)
564
+ else
565
+ self.column_names.dup
566
+ end
432
567
  end
433
568
 
434
- if models.first.id.nil? && column_names.include?(primary_key) && columns_hash[primary_key].type == :uuid
435
- column_names.delete(primary_key)
569
+ if models.first.id.nil?
570
+ Array(primary_key).each do |c|
571
+ if column_names.include?(c) && columns_hash[c].type == :uuid
572
+ column_names.delete(c)
573
+ end
574
+ end
436
575
  end
437
576
 
438
577
  default_values = column_defaults
@@ -444,11 +583,34 @@ class ActiveRecord::Base
444
583
  serialized_attributes
445
584
  end
446
585
 
447
- array_of_attributes = models.map do |model|
448
- column_names.map do |name|
449
- if stored_attrs.key?(name.to_sym) ||
450
- serialized_attrs.key?(name) ||
451
- default_values.key?(name.to_s)
586
+ update_attrs = if record_timestamps && options[:timestamps]
587
+ if respond_to?(:timestamp_attributes_for_update, true)
588
+ send(:timestamp_attributes_for_update).map(&:to_sym)
589
+ else
590
+ new.send(:timestamp_attributes_for_update_in_model)
591
+ end
592
+ end
593
+
594
+ array_of_attributes = []
595
+
596
+ models.each do |model|
597
+ if supports_setting_primary_key_of_imported_objects?
598
+ load_association_ids(model)
599
+ end
600
+
601
+ if is_validating && !validator.valid_model?(model)
602
+ raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
603
+ next
604
+ end
605
+
606
+ array_of_attributes << column_names.map do |name|
607
+ if model.persisted? &&
608
+ update_attrs && update_attrs.include?(name.to_sym) &&
609
+ !model.send("#{name}_changed?")
610
+ nil
611
+ elsif stored_attrs.key?(name.to_sym) ||
612
+ serialized_attrs.key?(name.to_s) ||
613
+ default_values[name.to_s]
452
614
  model.read_attribute(name.to_s)
453
615
  else
454
616
  model.read_attribute_before_type_cast(name.to_s)
@@ -460,12 +622,18 @@ class ActiveRecord::Base
460
622
  if args.length == 2
461
623
  array_of_hashes = args.last
462
624
  column_names = args.first.dup
625
+ allow_extra_hash_keys = true
463
626
  else
464
627
  array_of_hashes = args.first
465
628
  column_names = array_of_hashes.first.keys
629
+ allow_extra_hash_keys = false
466
630
  end
467
631
 
468
632
  array_of_attributes = array_of_hashes.map do |h|
633
+ error_message = validate_hash_import(h, column_names, allow_extra_hash_keys)
634
+
635
+ raise ArgumentError, error_message if error_message
636
+
469
637
  column_names.map do |key|
470
638
  h[key]
471
639
  end
@@ -497,47 +665,78 @@ class ActiveRecord::Base
497
665
 
498
666
  if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && connection.prefetch_primary_key? && sequence_name
499
667
  column_count = column_names.size
500
- column_names.concat(primary_key).uniq!
668
+ column_names.concat(Array(primary_key)).uniq!
501
669
  columns_added = column_names.size - column_count
502
670
  new_fields = Array.new(columns_added)
503
671
  array_of_attributes.each { |a| a.concat(new_fields) }
504
672
  end
505
673
 
674
+ # Don't modify incoming arguments
675
+ on_duplicate_key_update = options[:on_duplicate_key_update]
676
+ if on_duplicate_key_update
677
+ updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
678
+ options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
679
+ on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
680
+ duped_options[k] = if k == :columns && v == :all
681
+ updatable_columns
682
+ elsif v.duplicable?
683
+ v.dup
684
+ else
685
+ v
686
+ end
687
+ end
688
+ elsif on_duplicate_key_update == :all
689
+ updatable_columns
690
+ elsif on_duplicate_key_update.duplicable?
691
+ on_duplicate_key_update.dup
692
+ else
693
+ on_duplicate_key_update
694
+ end
695
+ end
696
+
506
697
  timestamps = {}
507
698
 
508
699
  # record timestamps unless disabled in ActiveRecord::Base
509
- if record_timestamps && options.delete( :timestamps )
700
+ if record_timestamps && options[:timestamps]
510
701
  timestamps = add_special_rails_stamps column_names, array_of_attributes, options
511
702
  end
512
703
 
513
704
  return_obj = if is_validating
514
- if models
515
- import_with_validations( column_names, array_of_attributes, options ) do |validator, failed|
516
- models.each_with_index do |model, i|
517
- model = model.dup if options[:recursive]
518
- next if validator.valid_model? model
705
+ import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
706
+ if models
707
+ models.each { |m| failed_instances << m if m.errors.any? }
708
+ else
709
+ # create instances for each of our column/value sets
710
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
711
+
712
+ # keep track of the instance and the position it is currently at. if this fails
713
+ # validation we'll use the index to remove it from the array_of_attributes
714
+ arr.each_with_index do |hsh, i|
715
+ model = new
716
+ hsh.each_pair { |k, v| model[k] = v }
717
+ next if validator.valid_model?(model)
519
718
  raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
520
719
  array_of_attributes[i] = nil
521
- failed << model
720
+ failure = model.dup
721
+ failure.errors.send(:initialize_dup, model.errors)
722
+ failed_instances << failure
522
723
  end
724
+ array_of_attributes.compact!
523
725
  end
524
- else
525
- import_with_validations( column_names, array_of_attributes, options )
526
726
  end
527
727
  else
528
- (num_inserts, ids) = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
529
- ActiveRecord::Import::Result.new([], num_inserts, ids)
728
+ import_without_validations_or_callbacks( column_names, array_of_attributes, options )
530
729
  end
531
730
 
532
731
  if options[:synchronize]
533
- sync_keys = options[:synchronize_keys] || [primary_key]
732
+ sync_keys = options[:synchronize_keys] || Array(primary_key)
534
733
  synchronize( options[:synchronize], sync_keys)
535
734
  end
536
735
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
537
736
 
538
737
  # if we have ids, then set the id on the models and mark the models as clean.
539
- if models && support_setting_primary_key_of_imported_objects?
540
- set_attributes_and_mark_clean(models, return_obj, timestamps)
738
+ if models && supports_setting_primary_key_of_imported_objects?
739
+ set_attributes_and_mark_clean(models, return_obj, timestamps, options)
541
740
 
542
741
  # if there are auto-save associations on the models we imported that are new, import them as well
543
742
  import_associations(models, options.dup) if options[:recursive]
@@ -556,36 +755,14 @@ class ActiveRecord::Base
556
755
  def import_with_validations( column_names, array_of_attributes, options = {} )
557
756
  failed_instances = []
558
757
 
559
- validator = ActiveRecord::Import::Validator.new(options)
560
-
561
- if block_given?
562
- yield validator, failed_instances
563
- else
564
- # create instances for each of our column/value sets
565
- arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
566
-
567
- # keep track of the instance and the position it is currently at. if this fails
568
- # validation we'll use the index to remove it from the array_of_attributes
569
- model = new
570
- arr.each_with_index do |hsh, i|
571
- hsh.each_pair { |k, v| model[k] = v }
572
- next if validator.valid_model? model
573
- raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
574
- array_of_attributes[i] = nil
575
- failure = model.dup
576
- failure.errors.send(:initialize_dup, model.errors)
577
- failed_instances << failure
578
- end
579
- end
580
-
581
- array_of_attributes.compact!
758
+ yield failed_instances if block_given?
582
759
 
583
- num_inserts, ids = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
584
- [0, []]
760
+ result = if options[:all_or_none] && failed_instances.any?
761
+ ActiveRecord::Import::Result.new([], 0, [], [])
585
762
  else
586
763
  import_without_validations_or_callbacks( column_names, array_of_attributes, options )
587
764
  end
588
- ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids)
765
+ ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results)
589
766
  end
590
767
 
591
768
  # Imports the passed in +column_names+ and +array_of_attributes+
@@ -595,6 +772,8 @@ class ActiveRecord::Base
595
772
  # information on +column_names+, +array_of_attributes_ and
596
773
  # +options+.
597
774
  def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
775
+ return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
776
+
598
777
  column_names = column_names.map(&:to_sym)
599
778
  scope_columns, scope_values = scope_attributes.to_a.transpose
600
779
 
@@ -604,7 +783,7 @@ class ActiveRecord::Base
604
783
  next if column_names.include?(name_as_sym)
605
784
 
606
785
  is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
607
- value = value.first if is_sti
786
+ value = Array(value).first if is_sti
608
787
 
609
788
  column_names << name_as_sym
610
789
  array_of_attributes.each { |attrs| attrs << value }
@@ -627,6 +806,7 @@ class ActiveRecord::Base
627
806
 
628
807
  number_inserted = 0
629
808
  ids = []
809
+ results = []
630
810
  if supports_import?
631
811
  # generate the sql
632
812
  post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
@@ -637,9 +817,10 @@ class ActiveRecord::Base
637
817
  result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
638
818
  batch_values,
639
819
  options,
640
- "#{self.class.name} Create Many Without Validations Or Callbacks" )
641
- number_inserted += result[0]
642
- ids += result[1]
820
+ "#{model_name} Create Many Without Validations Or Callbacks" )
821
+ number_inserted += result.num_inserts
822
+ ids += result.ids
823
+ results += result.results
643
824
  end
644
825
  else
645
826
  transaction(requires_new: true) do
@@ -649,22 +830,14 @@ class ActiveRecord::Base
649
830
  end
650
831
  end
651
832
  end
652
- [number_inserted, ids]
833
+ ActiveRecord::Import::Result.new([], number_inserted, ids, results)
653
834
  end
654
835
 
655
836
  private
656
837
 
657
- def set_attributes_and_mark_clean(models, import_result, timestamps)
838
+ def set_attributes_and_mark_clean(models, import_result, timestamps, options)
658
839
  return if models.nil?
659
840
  models -= import_result.failed_instances
660
- models.each do |model|
661
- if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
662
- model.clear_changes_information
663
- else # Rails 3.2
664
- model.instance_variable_get(:@changed_attributes).clear
665
- end
666
- model.instance_variable_set(:@new_record, false)
667
- end
668
841
 
669
842
  # if ids were returned for all models we know all were updated
670
843
  if models.size == import_result.ids.size
@@ -677,6 +850,49 @@ class ActiveRecord::Base
677
850
  end
678
851
  end
679
852
  end
853
+
854
+ if models.size == import_result.results.size
855
+ columns = Array(options[:returning])
856
+ single_column = "#{columns.first}=" if columns.size == 1
857
+ import_result.results.each_with_index do |result, index|
858
+ model = models[index]
859
+
860
+ if single_column
861
+ model.send(single_column, result)
862
+ else
863
+ columns.each_with_index do |column, col_index|
864
+ model.send("#{column}=", result[col_index])
865
+ end
866
+ end
867
+ end
868
+ end
869
+
870
+ models.each do |model|
871
+ if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
872
+ model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
873
+ model.changes_applied
874
+ elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
875
+ model.clear_changes_information
876
+ else # Rails 3.2
877
+ model.instance_variable_get(:@changed_attributes).clear
878
+ end
879
+ model.instance_variable_set(:@new_record, false)
880
+ end
881
+ end
882
+
883
+ # Sync belongs_to association ids with foreign key field
884
+ def load_association_ids(model)
885
+ association_reflections = model.class.reflect_on_all_associations(:belongs_to)
886
+ association_reflections.each do |association_reflection|
887
+ column_name = association_reflection.foreign_key
888
+ next if association_reflection.options[:polymorphic]
889
+ association = model.association(association_reflection.name)
890
+ association = association.target
891
+ next if association.blank? || model.public_send(column_name).present?
892
+
893
+ association_primary_key = association_reflection.association_primary_key
894
+ model.public_send("#{column_name}=", association.send(association_primary_key))
895
+ end
680
896
  end
681
897
 
682
898
  def import_associations(models, options)
@@ -693,7 +909,7 @@ class ActiveRecord::Base
693
909
 
694
910
  associated_objects_by_class.each_value do |associations|
695
911
  associations.each_value do |associated_records|
696
- associated_records.first.class.import(associated_records, options) unless associated_records.empty?
912
+ associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
697
913
  end
698
914
  end
699
915
  end
@@ -702,6 +918,7 @@ class ActiveRecord::Base
702
918
  # of class => objects to import.
703
919
  def find_associated_objects_for_import(associated_objects_by_class, model)
704
920
  associated_objects_by_class[model.class.name] ||= {}
921
+ return associated_objects_by_class unless model.id
705
922
 
706
923
  association_reflections =
707
924
  model.class.reflect_on_all_associations(:has_one) +
@@ -740,8 +957,10 @@ class ActiveRecord::Base
740
957
  column = columns[j]
741
958
 
742
959
  # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
743
- if val.nil? && column.name == primary_key && !sequence_name.blank?
960
+ if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
744
961
  connection_memo.next_value_for_sequence(sequence_name)
962
+ elsif val.respond_to?(:to_sql)
963
+ "(#{val.to_sql})"
745
964
  elsif column
746
965
  if respond_to?(:type_caster) # Rails 5.0 and higher
747
966
  type = type_for_attribute(column.name)
@@ -753,7 +972,9 @@ class ActiveRecord::Base
753
972
  if serialized_attributes.include?(column.name)
754
973
  val = serialized_attributes[column.name].dump(val)
755
974
  end
756
- connection_memo.quote(column.type_cast(val), column)
975
+ # Fixes #443 to support binary (i.e. bytea) columns on PG
976
+ val = column.type_cast(val) unless column.type.to_sym == :binary
977
+ connection_memo.quote(val, column)
757
978
  end
758
979
  end
759
980
  end
@@ -785,7 +1006,7 @@ class ActiveRecord::Base
785
1006
  index = column_names.index(column) || column_names.index(column.to_sym)
786
1007
  if index
787
1008
  # replace every instance of the array of attributes with our value
788
- array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? || action == :update }
1009
+ array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
789
1010
  else
790
1011
  column_names << column
791
1012
  array_of_attributes.each { |arr| arr << timestamp }
@@ -804,5 +1025,42 @@ class ActiveRecord::Base
804
1025
  def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
805
1026
  array_of_attributes.map { |values| Hash[column_names.zip(values)] }
806
1027
  end
1028
+
1029
+ # Checks that the imported hash has the required_keys, optionally also checks that the hash has
1030
+ # no keys beyond those required when `allow_extra_keys` is false.
1031
+ # returns `nil` if validation passes, or an error message if it fails
1032
+ def validate_hash_import(hash, required_keys, allow_extra_keys) # :nodoc:
1033
+ extra_keys = allow_extra_keys ? [] : hash.keys - required_keys
1034
+ missing_keys = required_keys - hash.keys
1035
+
1036
+ return nil if extra_keys.empty? && missing_keys.empty?
1037
+
1038
+ if allow_extra_keys
1039
+ <<-EOS
1040
+ Hash key mismatch.
1041
+
1042
+ When importing an array of hashes with provided columns_names, each hash must contain keys for all column_names.
1043
+
1044
+ Required keys: #{required_keys}
1045
+ Missing keys: #{missing_keys}
1046
+
1047
+ Hash: #{hash}
1048
+ EOS
1049
+ else
1050
+ <<-EOS
1051
+ Hash key mismatch.
1052
+
1053
+ When importing an array of hashes, all hashes must have the same keys.
1054
+ If you have records that are missing some values, we recommend you either set default values
1055
+ for the missing keys or group these records into batches by key set before importing.
1056
+
1057
+ Required keys: #{required_keys}
1058
+ Extra keys: #{extra_keys}
1059
+ Missing keys: #{missing_keys}
1060
+
1061
+ Hash: #{hash}
1062
+ EOS
1063
+ end
1064
+ end
807
1065
  end
808
1066
  end