activerecord-import 0.22.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +235 -3
  7. data/Gemfile +22 -15
  8. data/LICENSE +21 -56
  9. data/README.markdown +574 -22
  10. data/Rakefile +4 -1
  11. data/activerecord-import.gemspec +6 -5
  12. data/benchmarks/benchmark.rb +7 -1
  13. data/benchmarks/lib/base.rb +2 -0
  14. data/benchmarks/lib/cli_parser.rb +3 -1
  15. data/benchmarks/lib/float.rb +2 -0
  16. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  17. data/benchmarks/lib/output_to_csv.rb +2 -0
  18. data/benchmarks/lib/output_to_html.rb +4 -2
  19. data/benchmarks/models/test_innodb.rb +2 -0
  20. data/benchmarks/models/test_memory.rb +2 -0
  21. data/benchmarks/models/test_myisam.rb +2 -0
  22. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  23. data/gemfiles/4.2.gemfile +2 -0
  24. data/gemfiles/5.0.gemfile +2 -0
  25. data/gemfiles/5.1.gemfile +2 -0
  26. data/gemfiles/5.2.gemfile +4 -0
  27. data/gemfiles/6.0.gemfile +4 -0
  28. data/gemfiles/6.1.gemfile +4 -0
  29. data/gemfiles/7.0.gemfile +4 -0
  30. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  32. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  33. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  38. data/lib/activerecord-import/adapters/abstract_adapter.rb +10 -2
  39. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  40. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql_adapter.rb +19 -11
  42. data/lib/activerecord-import/adapters/postgresql_adapter.rb +56 -37
  43. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
  44. data/lib/activerecord-import/base.rb +12 -2
  45. data/lib/activerecord-import/import.rb +300 -136
  46. data/lib/activerecord-import/mysql2.rb +2 -0
  47. data/lib/activerecord-import/postgresql.rb +2 -0
  48. data/lib/activerecord-import/sqlite3.rb +2 -0
  49. data/lib/activerecord-import/synchronize.rb +4 -2
  50. data/lib/activerecord-import/value_sets_parser.rb +4 -0
  51. data/lib/activerecord-import/version.rb +3 -1
  52. data/lib/activerecord-import.rb +4 -1
  53. data/test/adapters/jdbcmysql.rb +2 -0
  54. data/test/adapters/jdbcpostgresql.rb +2 -0
  55. data/test/adapters/jdbcsqlite3.rb +2 -0
  56. data/test/adapters/makara_postgis.rb +3 -0
  57. data/test/adapters/mysql2.rb +2 -0
  58. data/test/adapters/mysql2_makara.rb +2 -0
  59. data/test/adapters/mysql2spatial.rb +2 -0
  60. data/test/adapters/postgis.rb +2 -0
  61. data/test/adapters/postgresql.rb +2 -0
  62. data/test/adapters/postgresql_makara.rb +2 -0
  63. data/test/adapters/seamless_database_pool.rb +2 -0
  64. data/test/adapters/spatialite.rb +2 -0
  65. data/test/adapters/sqlite3.rb +2 -0
  66. data/test/{travis → github}/database.yml +3 -1
  67. data/test/import_test.rb +159 -8
  68. data/test/jdbcmysql/import_test.rb +2 -0
  69. data/test/jdbcpostgresql/import_test.rb +2 -0
  70. data/test/jdbcsqlite3/import_test.rb +2 -0
  71. data/test/makara_postgis/import_test.rb +10 -0
  72. data/test/models/account.rb +5 -0
  73. data/test/models/alarm.rb +2 -0
  74. data/test/models/animal.rb +8 -0
  75. data/test/models/bike_maker.rb +9 -0
  76. data/test/models/book.rb +2 -0
  77. data/test/models/car.rb +2 -0
  78. data/test/models/card.rb +5 -0
  79. data/test/models/chapter.rb +2 -0
  80. data/test/models/customer.rb +8 -0
  81. data/test/models/deck.rb +8 -0
  82. data/test/models/dictionary.rb +2 -0
  83. data/test/models/discount.rb +2 -0
  84. data/test/models/end_note.rb +2 -0
  85. data/test/models/group.rb +2 -0
  86. data/test/models/order.rb +8 -0
  87. data/test/models/playing_card.rb +4 -0
  88. data/test/models/promotion.rb +2 -0
  89. data/test/models/question.rb +2 -0
  90. data/test/models/rule.rb +2 -0
  91. data/test/models/tag.rb +3 -0
  92. data/test/models/tag_alias.rb +5 -0
  93. data/test/models/topic.rb +2 -0
  94. data/test/models/user.rb +5 -0
  95. data/test/models/user_token.rb +6 -0
  96. data/test/models/vendor.rb +2 -0
  97. data/test/models/widget.rb +2 -0
  98. data/test/mysql2/import_test.rb +2 -0
  99. data/test/mysql2_makara/import_test.rb +2 -0
  100. data/test/mysqlspatial2/import_test.rb +2 -0
  101. data/test/postgis/import_test.rb +2 -0
  102. data/test/postgresql/import_test.rb +2 -0
  103. data/test/schema/generic_schema.rb +53 -0
  104. data/test/schema/jdbcpostgresql_schema.rb +2 -0
  105. data/test/schema/mysql2_schema.rb +21 -0
  106. data/test/schema/postgis_schema.rb +2 -0
  107. data/test/schema/postgresql_schema.rb +18 -0
  108. data/test/schema/sqlite3_schema.rb +15 -0
  109. data/test/schema/version.rb +2 -0
  110. data/test/sqlite3/import_test.rb +2 -0
  111. data/test/support/active_support/test_case_extensions.rb +2 -0
  112. data/test/support/assertions.rb +2 -0
  113. data/test/support/factories.rb +10 -8
  114. data/test/support/generate.rb +10 -8
  115. data/test/support/mysql/import_examples.rb +14 -1
  116. data/test/support/postgresql/import_examples.rb +140 -3
  117. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  118. data/test/support/shared_examples/on_duplicate_key_update.rb +263 -0
  119. data/test/support/shared_examples/recursive_import.rb +76 -4
  120. data/test/support/sqlite3/import_examples.rb +191 -26
  121. data/test/synchronize_test.rb +2 -0
  122. data/test/test_helper.rb +36 -3
  123. data/test/value_sets_bytes_parser_test.rb +2 -0
  124. data/test/value_sets_records_parser_test.rb +2 -0
  125. metadata +46 -18
  126. data/.travis.yml +0 -61
  127. data/gemfiles/3.2.gemfile +0 -2
  128. data/gemfiles/4.0.gemfile +0 -2
  129. data/gemfiles/4.1.gemfile +0 -2
  130. data/test/schema/mysql_schema.rb +0 -16
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ostruct"
2
4
 
3
5
  module ActiveRecord::Import::ConnectionAdapters; end
@@ -24,27 +26,66 @@ module ActiveRecord::Import #:nodoc:
24
26
  end
25
27
 
26
28
  class Validator
27
- def initialize(options = {})
29
+ def initialize(klass, options = {})
28
30
  @options = options
31
+ @validator_class = klass
32
+ init_validations(klass)
33
+ end
34
+
35
+ def init_validations(klass)
36
+ @validate_callbacks = klass._validate_callbacks.dup
37
+
38
+ @validate_callbacks.each_with_index do |callback, i|
39
+ filter = callback.respond_to?(:raw_filter) ? callback.raw_filter : callback.filter
40
+ next unless filter.class.name =~ /Validations::PresenceValidator/ ||
41
+ (!@options[:validate_uniqueness] &&
42
+ filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
43
+
44
+ callback = callback.dup
45
+ filter = filter.dup
46
+ attrs = filter.instance_variable_get(:@attributes).dup
47
+
48
+ if filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
49
+ attrs = []
50
+ else
51
+ associations = klass.reflect_on_all_associations(:belongs_to)
52
+ associations.each do |assoc|
53
+ if (index = attrs.index(assoc.name))
54
+ key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
55
+ attrs[index] = key unless attrs.include?(key)
56
+ end
57
+ end
58
+ end
59
+
60
+ filter.instance_variable_set(:@attributes, attrs.flatten)
61
+
62
+ if @validate_callbacks.respond_to?(:chain, true)
63
+ @validate_callbacks.send(:chain).tap do |chain|
64
+ callback.instance_variable_set(:@filter, filter)
65
+ chain[i] = callback
66
+ end
67
+ else
68
+ callback.raw_filter = filter
69
+ callback.filter = callback.send(:_compile_filter, filter)
70
+ @validate_callbacks[i] = callback
71
+ end
72
+ end
29
73
  end
30
74
 
31
75
  def valid_model?(model)
76
+ init_validations(model.class) unless model.class == @validator_class
77
+
32
78
  validation_context = @options[:validate_with_context]
33
79
  validation_context ||= (model.new_record? ? :create : :update)
34
-
35
80
  current_context = model.send(:validation_context)
81
+
36
82
  begin
37
83
  model.send(:validation_context=, validation_context)
38
84
  model.errors.clear
39
85
 
40
- validate_callbacks = model._validate_callbacks.dup
41
- model._validate_callbacks.each do |callback|
42
- validate_callbacks.delete(callback) if callback.raw_filter.is_a? ActiveRecord::Validations::UniquenessValidator
43
- end
44
-
45
86
  model.run_callbacks(:validation) do
46
87
  if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
47
- runner = validate_callbacks.compile
88
+ runner = @validate_callbacks.compile
48
89
  env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
49
90
  if runner.respond_to?(:call) # ActiveRecord < 5.1
50
91
  runner.call(env)
@@ -63,10 +104,10 @@ module ActiveRecord::Import #:nodoc:
63
104
  runner.invoke_before(env)
64
105
  runner.invoke_after(env)
65
106
  end
66
- elsif validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
67
- model.instance_eval validate_callbacks.compile
107
+ elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
108
+ model.instance_eval @validate_callbacks.compile
68
109
  else # ActiveRecord 3.x
69
- model.instance_eval validate_callbacks.compile(nil, model)
110
+ model.instance_eval @validate_callbacks.compile(nil, model)
70
111
  end
71
112
  end
72
113
 
@@ -79,13 +120,14 @@ module ActiveRecord::Import #:nodoc:
79
120
  end
80
121
 
81
122
  class ActiveRecord::Associations::CollectionProxy
82
- def import(*args, &block)
83
- @association.import(*args, &block)
123
+ def bulk_import(*args, &block)
124
+ @association.bulk_import(*args, &block)
84
125
  end
126
+ alias import bulk_import unless respond_to? :import
85
127
  end
86
128
 
87
129
  class ActiveRecord::Associations::CollectionAssociation
88
- def import(*args, &block)
130
+ def bulk_import(*args, &block)
89
131
  unless owner.persisted?
90
132
  raise ActiveRecord::RecordNotSaved, "You cannot call import unless the parent is saved"
91
133
  end
@@ -94,9 +136,14 @@ class ActiveRecord::Associations::CollectionAssociation
94
136
 
95
137
  model_klass = reflection.klass
96
138
  symbolized_foreign_key = reflection.foreign_key.to_sym
97
- symbolized_column_names = model_klass.column_names.map(&:to_sym)
98
139
 
99
- owner_primary_key = owner.class.primary_key
140
+ symbolized_column_names = if model_klass.connection.respond_to?(:supports_virtual_columns?) && model_klass.connection.supports_virtual_columns?
141
+ model_klass.columns.reject(&:virtual?).map { |c| c.name.to_sym }
142
+ else
143
+ model_klass.column_names.map(&:to_sym)
144
+ end
145
+
146
+ owner_primary_key = reflection.active_record_primary_key.to_sym
100
147
  owner_primary_key_value = owner.send(owner_primary_key)
101
148
 
102
149
  # assume array of model objects
@@ -118,7 +165,7 @@ class ActiveRecord::Associations::CollectionAssociation
118
165
  m.public_send "#{reflection.type}=", owner.class.name if reflection.type
119
166
  end
120
167
 
121
- return model_klass.import column_names, models, options
168
+ return model_klass.bulk_import column_names, models, options
122
169
 
123
170
  # supports array of hash objects
124
171
  elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
@@ -157,7 +204,7 @@ class ActiveRecord::Associations::CollectionAssociation
157
204
  end
158
205
  end
159
206
 
160
- return model_klass.import column_names, array_of_attributes, options
207
+ return model_klass.bulk_import column_names, array_of_attributes, options
161
208
 
162
209
  # supports empty array
163
210
  elsif args.last.is_a?( Array ) && args.last.empty?
@@ -192,23 +239,25 @@ class ActiveRecord::Associations::CollectionAssociation
192
239
  end
193
240
  end
194
241
 
195
- return model_klass.import column_names, array_of_attributes, options
242
+ return model_klass.bulk_import column_names, array_of_attributes, options
196
243
  else
197
244
  raise ArgumentError, "Invalid arguments!"
198
245
  end
199
246
  end
247
+ alias import bulk_import unless respond_to? :import
248
+ end
249
+
250
+ module ActiveRecord::Import::Connection
251
+ def establish_connection(args = nil)
252
+ conn = super(args)
253
+ ActiveRecord::Import.load_from_connection_pool connection_pool
254
+ conn
255
+ end
200
256
  end
201
257
 
202
258
  class ActiveRecord::Base
203
259
  class << self
204
- def establish_connection_with_activerecord_import(*args)
205
- conn = establish_connection_without_activerecord_import(*args)
206
- ActiveRecord::Import.load_from_connection_pool connection_pool
207
- conn
208
- end
209
-
210
- alias establish_connection_without_activerecord_import establish_connection
211
- alias establish_connection establish_connection_with_activerecord_import
260
+ prepend ActiveRecord::Import::Connection
212
261
 
213
262
  # Returns true if the current database connection adapter
214
263
  # supports import functionality, otherwise returns false.
@@ -274,6 +323,9 @@ class ActiveRecord::Base
274
323
  # == Options
275
324
  # * +validate+ - true|false, tells import whether or not to use
276
325
  # ActiveRecord validations. Validations are enforced by default.
326
+ # It skips the uniqueness validation for performance reasons.
327
+ # You can find more details here:
328
+ # https://github.com/zdennis/activerecord-import/issues/228
277
329
  # * +ignore+ - true|false, an alias for on_duplicate_key_ignore.
278
330
  # * +on_duplicate_key_ignore+ - true|false, tells import to discard
279
331
  # records that contain duplicate keys. For Postgres 9.5+ it adds
@@ -282,8 +334,8 @@ class ActiveRecord::Base
282
334
  # recursive import. For database adapters that normally support
283
335
  # setting primary keys on imported objects, this option prevents
284
336
  # that from occurring.
285
- # * +on_duplicate_key_update+ - an Array or Hash, tells import to
286
- # use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
337
+ # * +on_duplicate_key_update+ - :all, an Array, or Hash, tells import to
338
+ # use MySQL's ON DUPLICATE KEY UPDATE or Postgres/SQLite ON CONFLICT
287
339
  # DO UPDATE ability. See On Duplicate Key Update below.
288
340
  # * +synchronize+ - an array of ActiveRecord instances for the model
289
341
  # that you are currently importing data into. This synchronizes
@@ -342,7 +394,15 @@ class ActiveRecord::Base
342
394
  #
343
395
  # == On Duplicate Key Update (MySQL)
344
396
  #
345
- # The :on_duplicate_key_update option can be either an Array or a Hash.
397
+ # The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
398
+ #
399
+ # ==== Using :all
400
+ #
401
+ # The :on_duplicate_key_update option can be set to :all. All columns
402
+ # other than the primary key are updated. If a list of column names is
403
+ # supplied, only those columns will be updated. Below is an example:
404
+ #
405
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
346
406
  #
347
407
  # ==== Using an Array
348
408
  #
@@ -361,11 +421,19 @@ class ActiveRecord::Base
361
421
  #
362
422
  # BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
363
423
  #
364
- # == On Duplicate Key Update (Postgres 9.5+)
424
+ # == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
365
425
  #
366
- # The :on_duplicate_key_update option can be an Array or a Hash with up to
426
+ # The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
367
427
  # three attributes, :conflict_target (and optionally :index_predicate) or
368
- # :constraint_name, and :columns.
428
+ # :constraint_name (Postgres), and :columns.
429
+ #
430
+ # ==== Using :all
431
+ #
432
+ # The :on_duplicate_key_update option can be set to :all. All columns
433
+ # other than the primary key are updated. If a list of column names is
434
+ # supplied, only those columns will be updated. Below is an example:
435
+ #
436
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
369
437
  #
370
438
  # ==== Using an Array
371
439
  #
@@ -423,7 +491,15 @@ class ActiveRecord::Base
423
491
  #
424
492
  # ===== :columns
425
493
  #
426
- # The :columns attribute can be either an Array or a Hash.
494
+ # The :columns attribute can be either :all, an Array, or a Hash.
495
+ #
496
+ # ===== Using :all
497
+ #
498
+ # The :columns attribute can be :all. All columns other than the primary key will be updated.
499
+ # If a list of column names is supplied, only those columns will be updated.
500
+ # Below is an example:
501
+ #
502
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
427
503
  #
428
504
  # ===== Using an Array
429
505
  #
@@ -447,7 +523,7 @@ class ActiveRecord::Base
447
523
  # * num_inserts - the number of insert statements it took to import the data
448
524
  # * ids - the primary keys of the imported ids if the adapter supports it, otherwise an empty array.
449
525
  # * results - import results if the adapter supports it, otherwise an empty array.
450
- def import(*args)
526
+ def bulk_import(*args)
451
527
  if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
452
528
  options = {}
453
529
  options.merge!( args.pop ) if args.last.is_a?(Hash)
@@ -458,36 +534,29 @@ class ActiveRecord::Base
458
534
  import_helper(*args)
459
535
  end
460
536
  end
537
+ alias import bulk_import unless ActiveRecord::Base.respond_to? :import
461
538
 
462
539
  # Imports a collection of values if all values are valid. Import fails at the
463
540
  # first encountered validation error and raises ActiveRecord::RecordInvalid
464
541
  # with the failed instance.
465
- def import!(*args)
542
+ def bulk_import!(*args)
466
543
  options = args.last.is_a?( Hash ) ? args.pop : {}
467
544
  options[:validate] = true
468
545
  options[:raise_error] = true
469
546
 
470
- import(*args, options)
547
+ bulk_import(*args, options)
471
548
  end
549
+ alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
472
550
 
473
551
  def import_helper( *args )
474
- options = { validate: true, timestamps: true }
552
+ options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
475
553
  options.merge!( args.pop ) if args.last.is_a? Hash
476
554
  # making sure that current model's primary key is used
477
555
  options[:primary_key] = primary_key
556
+ options[:locking_column] = locking_column if attribute_names.include?(locking_column)
478
557
 
479
- # Don't modify incoming arguments
480
- on_duplicate_key_update = options[:on_duplicate_key_update]
481
- if on_duplicate_key_update && on_duplicate_key_update.duplicable?
482
- options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
483
- on_duplicate_key_update.each { |k, v| on_duplicate_key_update[k] = v.dup if v.duplicable? }
484
- else
485
- on_duplicate_key_update.dup
486
- end
487
- end
488
-
489
- is_validating = options[:validate]
490
- is_validating = true unless options[:validate_with_context].nil?
558
+ is_validating = options[:validate_with_context].present? ? true : options[:validate]
559
+ validator = ActiveRecord::Import::Validator.new(self, options)
491
560
 
492
561
  # assume array of model objects
493
562
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
@@ -496,34 +565,48 @@ class ActiveRecord::Base
496
565
  column_names = args.first.dup
497
566
  else
498
567
  models = args.first
499
- column_names = self.column_names.dup
568
+ column_names = if connection.respond_to?(:supports_virtual_columns?) && connection.supports_virtual_columns?
569
+ columns.reject(&:virtual?).map(&:name)
570
+ else
571
+ self.column_names.dup
572
+ end
500
573
  end
501
574
 
502
- if models.first.id.nil? && column_names.include?(primary_key) && columns_hash[primary_key].type == :uuid
503
- column_names.delete(primary_key)
575
+ if models.first.id.nil?
576
+ Array(primary_key).each do |c|
577
+ if column_names.include?(c) && columns_hash[c].type == :uuid
578
+ column_names.delete(c)
579
+ end
580
+ end
504
581
  end
505
582
 
506
- default_values = column_defaults
507
- stored_attrs = respond_to?(:stored_attributes) ? stored_attributes : {}
508
- serialized_attrs = if defined?(ActiveRecord::Type::Serialized)
509
- attrs = column_names.select { |c| type_for_attribute(c.to_s).class == ActiveRecord::Type::Serialized }
510
- Hash[attrs.map { |a| [a, nil] }]
511
- else
512
- serialized_attributes
583
+ update_attrs = if record_timestamps && options[:timestamps]
584
+ if respond_to?(:timestamp_attributes_for_update, true)
585
+ send(:timestamp_attributes_for_update).map(&:to_sym)
586
+ else
587
+ allocate.send(:timestamp_attributes_for_update_in_model)
588
+ end
513
589
  end
514
590
 
515
- array_of_attributes = models.map do |model|
591
+ array_of_attributes = []
592
+
593
+ models.each do |model|
516
594
  if supports_setting_primary_key_of_imported_objects?
517
595
  load_association_ids(model)
518
596
  end
519
597
 
520
- column_names.map do |name|
521
- if stored_attrs.key?(name.to_sym) ||
522
- serialized_attrs.key?(name) ||
523
- default_values.key?(name.to_s)
524
- model.read_attribute(name.to_s)
598
+ if is_validating && !validator.valid_model?(model)
599
+ raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
600
+ next
601
+ end
602
+
603
+ array_of_attributes << column_names.map do |name|
604
+ if model.persisted? &&
605
+ update_attrs && update_attrs.include?(name.to_sym) &&
606
+ !model.send("#{name}_changed?")
607
+ nil
525
608
  else
526
- model.read_attribute_before_type_cast(name.to_s)
609
+ model.read_attribute(name.to_s)
527
610
  end
528
611
  end
529
612
  end
@@ -550,7 +633,7 @@ class ActiveRecord::Base
550
633
  end
551
634
  # supports empty array
552
635
  elsif args.last.is_a?( Array ) && args.last.empty?
553
- return ActiveRecord::Import::Result.new([], 0, [])
636
+ return ActiveRecord::Import::Result.new([], 0, [], [])
554
637
  # supports 2-element array and array
555
638
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
556
639
 
@@ -581,32 +664,69 @@ class ActiveRecord::Base
581
664
  array_of_attributes.each { |a| a.concat(new_fields) }
582
665
  end
583
666
 
667
+ # Don't modify incoming arguments
668
+ on_duplicate_key_update = options[:on_duplicate_key_update]
669
+ if on_duplicate_key_update
670
+ updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
671
+ options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
672
+ on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
673
+ duped_options[k] = if k == :columns && v == :all
674
+ updatable_columns
675
+ elsif v.duplicable?
676
+ v.dup
677
+ else
678
+ v
679
+ end
680
+ end
681
+ elsif on_duplicate_key_update == :all
682
+ updatable_columns
683
+ elsif on_duplicate_key_update.duplicable?
684
+ on_duplicate_key_update.dup
685
+ else
686
+ on_duplicate_key_update
687
+ end
688
+ end
689
+
584
690
  timestamps = {}
585
691
 
586
692
  # record timestamps unless disabled in ActiveRecord::Base
587
- if record_timestamps && options.delete( :timestamps )
693
+ if record_timestamps && options[:timestamps]
588
694
  timestamps = add_special_rails_stamps column_names, array_of_attributes, options
589
695
  end
590
696
 
591
697
  return_obj = if is_validating
592
- if models
593
- import_with_validations( column_names, array_of_attributes, options ) do |validator, failed|
594
- models.each_with_index do |model, i|
595
- next if validator.valid_model? model
698
+ import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
699
+ if models
700
+ models.each { |m| failed_instances << m if m.errors.any? }
701
+ else
702
+ # create instances for each of our column/value sets
703
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
704
+
705
+ # keep track of the instance and the position it is currently at. if this fails
706
+ # validation we'll use the index to remove it from the array_of_attributes
707
+ arr.each_with_index do |hsh, i|
708
+ # utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
709
+ model = new do |m|
710
+ hsh.each_pair { |k, v| m[k] = v }
711
+ end
712
+
713
+ next if validator.valid_model?(model)
596
714
  raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
715
+
597
716
  array_of_attributes[i] = nil
598
- failed << model
717
+ failure = model.dup
718
+ failure.errors.send(:initialize_dup, model.errors)
719
+ failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
599
720
  end
721
+ array_of_attributes.compact!
600
722
  end
601
- else
602
- import_with_validations( column_names, array_of_attributes, options )
603
723
  end
604
724
  else
605
725
  import_without_validations_or_callbacks( column_names, array_of_attributes, options )
606
726
  end
607
727
 
608
728
  if options[:synchronize]
609
- sync_keys = options[:synchronize_keys] || [primary_key]
729
+ sync_keys = options[:synchronize_keys] || Array(primary_key)
610
730
  synchronize( options[:synchronize], sync_keys)
611
731
  end
612
732
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
@@ -616,7 +736,10 @@ class ActiveRecord::Base
616
736
  set_attributes_and_mark_clean(models, return_obj, timestamps, options)
617
737
 
618
738
  # if there are auto-save associations on the models we imported that are new, import them as well
619
- import_associations(models, options.dup) if options[:recursive]
739
+ if options[:recursive]
740
+ options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
741
+ import_associations(models, options.dup.merge(validate: false))
742
+ end
620
743
  end
621
744
 
622
745
  return_obj
@@ -632,31 +755,9 @@ class ActiveRecord::Base
632
755
  def import_with_validations( column_names, array_of_attributes, options = {} )
633
756
  failed_instances = []
634
757
 
635
- validator = ActiveRecord::Import::Validator.new(options)
758
+ yield failed_instances if block_given?
636
759
 
637
- if block_given?
638
- yield validator, failed_instances
639
- else
640
- # create instances for each of our column/value sets
641
- arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
642
-
643
- # keep track of the instance and the position it is currently at. if this fails
644
- # validation we'll use the index to remove it from the array_of_attributes
645
- arr.each_with_index do |hsh, i|
646
- model = new
647
- hsh.each_pair { |k, v| model[k] = v }
648
- next if validator.valid_model? model
649
- raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
650
- array_of_attributes[i] = nil
651
- failure = model.dup
652
- failure.errors.send(:initialize_dup, model.errors)
653
- failed_instances << failure
654
- end
655
- end
656
-
657
- array_of_attributes.compact!
658
-
659
- result = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
760
+ result = if options[:all_or_none] && failed_instances.any?
660
761
  ActiveRecord::Import::Result.new([], 0, [], [])
661
762
  else
662
763
  import_without_validations_or_callbacks( column_names, array_of_attributes, options )
@@ -671,27 +772,30 @@ class ActiveRecord::Base
671
772
  # information on +column_names+, +array_of_attributes_ and
672
773
  # +options+.
673
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
+
674
777
  column_names = column_names.map(&:to_sym)
675
778
  scope_columns, scope_values = scope_attributes.to_a.transpose
676
779
 
677
780
  unless scope_columns.blank?
678
781
  scope_columns.zip(scope_values).each do |name, value|
679
782
  name_as_sym = name.to_sym
680
- next if column_names.include?(name_as_sym)
681
-
682
- is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
683
- value = Array(value).first if is_sti
684
-
783
+ next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
685
784
  column_names << name_as_sym
686
785
  array_of_attributes.each { |attrs| attrs << value }
687
786
  end
688
787
  end
689
788
 
789
+ if finder_needs_type_condition?
790
+ unless column_names.include?(inheritance_column.to_sym)
791
+ column_names << inheritance_column.to_sym
792
+ array_of_attributes.each { |attrs| attrs << sti_name }
793
+ end
794
+ end
795
+
690
796
  columns = column_names.each_with_index.map do |name, i|
691
797
  column = columns_hash[name.to_s]
692
-
693
798
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
694
-
695
799
  column
696
800
  end
697
801
 
@@ -707,17 +811,29 @@ class ActiveRecord::Base
707
811
  if supports_import?
708
812
  # generate the sql
709
813
  post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
814
+ import_size = values_sql.size
815
+
816
+ batch_size = options[:batch_size] || import_size
817
+ run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
818
+ progress_proc = options[:batch_progress]
819
+ current_batch = 0
820
+ batches = (import_size / batch_size.to_f).ceil
710
821
 
711
- batch_size = options[:batch_size] || values_sql.size
712
822
  values_sql.each_slice(batch_size) do |batch_values|
823
+ batch_started_at = Time.now.to_i
824
+
713
825
  # perform the inserts
714
826
  result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
715
827
  batch_values,
716
828
  options,
717
- "#{model_name} Create Many Without Validations Or Callbacks" )
829
+ "#{model_name} Create Many" )
830
+
718
831
  number_inserted += result.num_inserts
719
832
  ids += result.ids
720
833
  results += result.results
834
+ current_batch += 1
835
+
836
+ progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
721
837
  end
722
838
  else
723
839
  transaction(requires_new: true) do
@@ -743,29 +859,56 @@ class ActiveRecord::Base
743
859
  model.id = id
744
860
 
745
861
  timestamps.each do |attr, value|
746
- model.send(attr + "=", value)
862
+ model.send(attr + "=", value) if model.send(attr).nil?
747
863
  end
748
864
  end
749
865
  end
750
866
 
751
- if models.size == import_result.results.size
752
- columns = Array(options[:returning])
753
- single_column = "#{columns.first}=" if columns.size == 1
754
- import_result.results.each_with_index do |result, index|
867
+ deserialize_value = lambda do |column, value|
868
+ column = columns_hash[column]
869
+ return value unless column
870
+ if respond_to?(:type_caster)
871
+ type = type_for_attribute(column.name)
872
+ type.deserialize(value)
873
+ elsif column.respond_to?(:type_cast_from_database)
874
+ column.type_cast_from_database(value)
875
+ else
876
+ value
877
+ end
878
+ end
879
+
880
+ set_value = lambda do |model, column, value|
881
+ val = deserialize_value.call(column, value)
882
+ if model.attribute_names.include?(column)
883
+ model.send("#{column}=", val)
884
+ else
885
+ attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
886
+ model.instance_variable_set(:@attributes, attributes)
887
+ end
888
+ end
889
+
890
+ columns = Array(options[:returning_columns])
891
+ results = Array(import_result.results)
892
+ if models.size == results.size
893
+ single_column = columns.first if columns.size == 1
894
+ results.each_with_index do |result, index|
755
895
  model = models[index]
756
896
 
757
897
  if single_column
758
- model.send(single_column, result)
898
+ set_value.call(model, single_column, result)
759
899
  else
760
900
  columns.each_with_index do |column, col_index|
761
- model.send("#{column}=", result[col_index])
901
+ set_value.call(model, column, result[col_index])
762
902
  end
763
903
  end
764
904
  end
765
905
  end
766
906
 
767
907
  models.each do |model|
768
- if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
908
+ if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
909
+ model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
910
+ model.changes_applied
911
+ elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
769
912
  model.clear_changes_information
770
913
  else # Rails 3.2
771
914
  model.instance_variable_get(:@changed_attributes).clear
@@ -776,14 +919,21 @@ class ActiveRecord::Base
776
919
 
777
920
  # Sync belongs_to association ids with foreign key field
778
921
  def load_association_ids(model)
922
+ changed_columns = model.changed
779
923
  association_reflections = model.class.reflect_on_all_associations(:belongs_to)
780
924
  association_reflections.each do |association_reflection|
781
925
  next if association_reflection.options[:polymorphic]
782
- association = model.association(association_reflection.name)
783
- association = association.target
784
- if association
785
- association_primary_key = association_reflection.association_primary_key
786
- model.public_send("#{association_reflection.foreign_key}=", association.send(association_primary_key))
926
+
927
+ column_names = Array(association_reflection.foreign_key).map(&:to_s)
928
+ column_names.each_with_index do |column_name, column_index|
929
+ next if changed_columns.include?(column_name)
930
+
931
+ association = model.association(association_reflection.name)
932
+ association = association.target
933
+ next if association.blank? || model.public_send(column_name).present?
934
+
935
+ association_primary_key = Array(association_reflection.association_primary_key)[column_index]
936
+ model.public_send("#{column_name}=", association.send(association_primary_key))
787
937
  end
788
938
  end
789
939
  end
@@ -797,12 +947,14 @@ class ActiveRecord::Base
797
947
  associated_objects_by_class = {}
798
948
  models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
799
949
 
800
- # :on_duplicate_key_update not supported for associations
801
- options.delete(:on_duplicate_key_update)
950
+ # :on_duplicate_key_update only supported for all fields
951
+ options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
952
+ # :returning not supported for associations
953
+ options.delete(:returning)
802
954
 
803
955
  associated_objects_by_class.each_value do |associations|
804
956
  associations.each_value do |associated_records|
805
- associated_records.first.class.import(associated_records, options) unless associated_records.empty?
957
+ associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
806
958
  end
807
959
  end
808
960
  end
@@ -829,8 +981,13 @@ class ActiveRecord::Base
829
981
  changed_objects.each do |child|
830
982
  child.public_send("#{association_reflection.foreign_key}=", model.id)
831
983
  # For polymorphic associations
984
+ association_name = if model.class.respond_to?(:polymorphic_name)
985
+ model.class.polymorphic_name
986
+ else
987
+ model.class.base_class
988
+ end
832
989
  association_reflection.type.try do |type|
833
- child.public_send("#{type}=", model.class.base_class.name)
990
+ child.public_send("#{type}=", association_name)
834
991
  end
835
992
  end
836
993
  associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
@@ -850,14 +1007,14 @@ class ActiveRecord::Base
850
1007
  column = columns[j]
851
1008
 
852
1009
  # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
853
- if val.nil? && column.name == primary_key && !sequence_name.blank?
1010
+ if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
854
1011
  connection_memo.next_value_for_sequence(sequence_name)
855
1012
  elsif val.respond_to?(:to_sql)
856
1013
  "(#{val.to_sql})"
857
1014
  elsif column
858
1015
  if respond_to?(:type_caster) # Rails 5.0 and higher
859
1016
  type = type_for_attribute(column.name)
860
- val = type.type == :boolean ? type.cast(val) : type.serialize(val)
1017
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
861
1018
  connection_memo.quote(val)
862
1019
  elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
863
1020
  connection_memo.quote(column.type_cast_from_user(val), column)
@@ -866,9 +1023,11 @@ class ActiveRecord::Base
866
1023
  val = serialized_attributes[column.name].dump(val)
867
1024
  end
868
1025
  # Fixes #443 to support binary (i.e. bytea) columns on PG
869
- val = column.type_cast(val) unless column.type.to_sym == :binary
1026
+ val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
870
1027
  connection_memo.quote(val, column)
871
1028
  end
1029
+ else
1030
+ raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
872
1031
  end
873
1032
  end
874
1033
  "(#{my_values.join(',')})"
@@ -883,13 +1042,18 @@ class ActiveRecord::Base
883
1042
  timestamp_columns[:create] = timestamp_attributes_for_create_in_model
884
1043
  timestamp_columns[:update] = timestamp_attributes_for_update_in_model
885
1044
  else
886
- instance = new
1045
+ instance = allocate
887
1046
  timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
888
1047
  timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
889
1048
  end
890
1049
 
891
1050
  # use tz as set in ActiveRecord::Base
892
- timestamp = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
1051
+ default_timezone = if ActiveRecord.respond_to?(:default_timezone)
1052
+ ActiveRecord.default_timezone
1053
+ else
1054
+ ActiveRecord::Base.default_timezone
1055
+ end
1056
+ timestamp = default_timezone == :utc ? Time.now.utc : Time.now
893
1057
 
894
1058
  [:create, :update].each do |action|
895
1059
  timestamp_columns[action].each do |column|
@@ -899,7 +1063,7 @@ class ActiveRecord::Base
899
1063
  index = column_names.index(column) || column_names.index(column.to_sym)
900
1064
  if index
901
1065
  # replace every instance of the array of attributes with our value
902
- array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? || action == :update }
1066
+ array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
903
1067
  else
904
1068
  column_names << column
905
1069
  array_of_attributes.each { |arr| arr << timestamp }
@@ -934,7 +1098,7 @@ Hash key mismatch.
934
1098
 
935
1099
  When importing an array of hashes with provided columns_names, each hash must contain keys for all column_names.
936
1100
 
937
- Required keys: #{column_names}
1101
+ Required keys: #{required_keys}
938
1102
  Missing keys: #{missing_keys}
939
1103
 
940
1104
  Hash: #{hash}
@@ -947,7 +1111,7 @@ When importing an array of hashes, all hashes must have the same keys.
947
1111
  If you have records that are missing some values, we recommend you either set default values
948
1112
  for the missing keys or group these records into batches by key set before importing.
949
1113
 
950
- Required keys: #{column_names}
1114
+ Required keys: #{required_keys}
951
1115
  Extra keys: #{extra_keys}
952
1116
  Missing keys: #{missing_keys}
953
1117