activerecord-import 0.19.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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