activerecord-import 0.25.0 → 1.7.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 (141) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +151 -0
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +74 -8
  5. data/.rubocop_todo.yml +10 -16
  6. data/Brewfile +3 -1
  7. data/CHANGELOG.md +232 -2
  8. data/Dockerfile +23 -0
  9. data/Gemfile +26 -14
  10. data/LICENSE +21 -56
  11. data/README.markdown +612 -21
  12. data/Rakefile +4 -1
  13. data/activerecord-import.gemspec +6 -5
  14. data/benchmarks/benchmark.rb +10 -4
  15. data/benchmarks/lib/base.rb +4 -2
  16. data/benchmarks/lib/cli_parser.rb +4 -2
  17. data/benchmarks/lib/float.rb +2 -0
  18. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  19. data/benchmarks/lib/output_to_csv.rb +2 -0
  20. data/benchmarks/lib/output_to_html.rb +4 -2
  21. data/benchmarks/models/test_innodb.rb +2 -0
  22. data/benchmarks/models/test_memory.rb +2 -0
  23. data/benchmarks/models/test_myisam.rb +2 -0
  24. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  25. data/docker-compose.yml +34 -0
  26. data/gemfiles/4.2.gemfile +2 -0
  27. data/gemfiles/5.0.gemfile +2 -0
  28. data/gemfiles/5.1.gemfile +2 -0
  29. data/gemfiles/5.2.gemfile +2 -0
  30. data/gemfiles/6.0.gemfile +4 -0
  31. data/gemfiles/6.1.gemfile +4 -0
  32. data/gemfiles/7.0.gemfile +4 -0
  33. data/gemfiles/7.1.gemfile +3 -0
  34. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  36. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  38. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  39. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  40. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  41. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  42. data/lib/activerecord-import/active_record/adapters/trilogy_adapter.rb +8 -0
  43. data/lib/activerecord-import/adapters/abstract_adapter.rb +15 -6
  44. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  45. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  46. data/lib/activerecord-import/adapters/mysql_adapter.rb +34 -29
  47. data/lib/activerecord-import/adapters/postgresql_adapter.rb +74 -55
  48. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +138 -13
  49. data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
  50. data/lib/activerecord-import/base.rb +11 -2
  51. data/lib/activerecord-import/import.rb +290 -114
  52. data/lib/activerecord-import/mysql2.rb +2 -0
  53. data/lib/activerecord-import/postgresql.rb +2 -0
  54. data/lib/activerecord-import/sqlite3.rb +2 -0
  55. data/lib/activerecord-import/synchronize.rb +4 -2
  56. data/lib/activerecord-import/value_sets_parser.rb +5 -0
  57. data/lib/activerecord-import/version.rb +3 -1
  58. data/lib/activerecord-import.rb +2 -1
  59. data/test/adapters/jdbcmysql.rb +2 -0
  60. data/test/adapters/jdbcpostgresql.rb +2 -0
  61. data/test/adapters/jdbcsqlite3.rb +2 -0
  62. data/test/adapters/makara_postgis.rb +2 -0
  63. data/test/adapters/mysql2.rb +2 -0
  64. data/test/adapters/mysql2_makara.rb +2 -0
  65. data/test/adapters/mysql2spatial.rb +2 -0
  66. data/test/adapters/postgis.rb +2 -0
  67. data/test/adapters/postgresql.rb +2 -0
  68. data/test/adapters/postgresql_makara.rb +2 -0
  69. data/test/adapters/seamless_database_pool.rb +2 -0
  70. data/test/adapters/spatialite.rb +2 -0
  71. data/test/adapters/sqlite3.rb +2 -0
  72. data/test/adapters/trilogy.rb +9 -0
  73. data/test/database.yml.sample +7 -0
  74. data/test/{travis → github}/database.yml +7 -1
  75. data/test/import_test.rb +151 -8
  76. data/test/jdbcmysql/import_test.rb +5 -3
  77. data/test/jdbcpostgresql/import_test.rb +4 -2
  78. data/test/jdbcsqlite3/import_test.rb +4 -2
  79. data/test/makara_postgis/import_test.rb +4 -2
  80. data/test/models/account.rb +2 -0
  81. data/test/models/alarm.rb +2 -0
  82. data/test/models/animal.rb +8 -0
  83. data/test/models/author.rb +7 -0
  84. data/test/models/bike_maker.rb +3 -0
  85. data/test/models/book.rb +7 -2
  86. data/test/models/car.rb +2 -0
  87. data/test/models/card.rb +5 -0
  88. data/test/models/chapter.rb +2 -0
  89. data/test/models/composite_book.rb +19 -0
  90. data/test/models/composite_chapter.rb +9 -0
  91. data/test/models/customer.rb +18 -0
  92. data/test/models/deck.rb +8 -0
  93. data/test/models/dictionary.rb +2 -0
  94. data/test/models/discount.rb +2 -0
  95. data/test/models/end_note.rb +2 -0
  96. data/test/models/group.rb +2 -0
  97. data/test/models/order.rb +17 -0
  98. data/test/models/playing_card.rb +4 -0
  99. data/test/models/promotion.rb +2 -0
  100. data/test/models/question.rb +2 -0
  101. data/test/models/rule.rb +2 -0
  102. data/test/models/tag.rb +9 -1
  103. data/test/models/tag_alias.rb +11 -0
  104. data/test/models/topic.rb +7 -0
  105. data/test/models/user.rb +2 -0
  106. data/test/models/user_token.rb +3 -0
  107. data/test/models/vendor.rb +2 -0
  108. data/test/models/widget.rb +2 -0
  109. data/test/mysql2/import_test.rb +5 -3
  110. data/test/mysql2_makara/import_test.rb +5 -3
  111. data/test/mysqlspatial2/import_test.rb +5 -3
  112. data/test/postgis/import_test.rb +4 -2
  113. data/test/postgresql/import_test.rb +4 -2
  114. data/test/schema/generic_schema.rb +37 -1
  115. data/test/schema/jdbcpostgresql_schema.rb +3 -1
  116. data/test/schema/mysql2_schema.rb +2 -0
  117. data/test/schema/postgis_schema.rb +3 -1
  118. data/test/schema/postgresql_schema.rb +49 -0
  119. data/test/schema/sqlite3_schema.rb +15 -0
  120. data/test/schema/version.rb +2 -0
  121. data/test/sqlite3/import_test.rb +4 -2
  122. data/test/support/active_support/test_case_extensions.rb +2 -0
  123. data/test/support/assertions.rb +2 -0
  124. data/test/support/factories.rb +10 -8
  125. data/test/support/generate.rb +10 -8
  126. data/test/support/mysql/import_examples.rb +2 -1
  127. data/test/support/postgresql/import_examples.rb +152 -3
  128. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  129. data/test/support/shared_examples/on_duplicate_key_update.rb +122 -9
  130. data/test/support/shared_examples/recursive_import.rb +128 -2
  131. data/test/support/sqlite3/import_examples.rb +191 -26
  132. data/test/synchronize_test.rb +2 -0
  133. data/test/test_helper.rb +34 -7
  134. data/test/trilogy/import_test.rb +7 -0
  135. data/test/value_sets_bytes_parser_test.rb +3 -1
  136. data/test/value_sets_records_parser_test.rb +3 -1
  137. metadata +46 -16
  138. data/.travis.yml +0 -71
  139. data/gemfiles/3.2.gemfile +0 -2
  140. data/gemfiles/4.0.gemfile +0 -2
  141. data/gemfiles/4.1.gemfile +0 -2
@@ -1,18 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ostruct"
2
4
 
3
5
  module ActiveRecord::Import::ConnectionAdapters; end
4
6
 
5
- module ActiveRecord::Import #:nodoc:
7
+ module ActiveRecord::Import # :nodoc:
6
8
  Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
7
9
 
8
- module ImportSupport #:nodoc:
9
- def supports_import? #:nodoc:
10
+ module ImportSupport # :nodoc:
11
+ def supports_import? # :nodoc:
10
12
  true
11
13
  end
12
14
  end
13
15
 
14
- module OnDuplicateKeyUpdateSupport #:nodoc:
15
- def supports_on_duplicate_key_update? #:nodoc:
16
+ module OnDuplicateKeyUpdateSupport # :nodoc:
17
+ def supports_on_duplicate_key_update? # :nodoc:
16
18
  true
17
19
  end
18
20
  end
@@ -24,33 +26,70 @@ 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.instance_of?(@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
- associations = model.class.reflect_on_all_associations(:belongs_to).map(&:name)
42
-
43
- model._validate_callbacks.each do |callback|
44
- filter = callback.raw_filter
45
- if filter.is_a?(ActiveRecord::Validations::UniquenessValidator) ||
46
- (defined?(ActiveRecord::Validations::PresenceValidator) && filter.is_a?(ActiveRecord::Validations::PresenceValidator) && associations.include?(filter.attributes.first))
47
- validate_callbacks.delete(callback)
48
- end
49
- end
50
-
51
86
  model.run_callbacks(:validation) do
52
87
  if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
53
- runner = validate_callbacks.compile
88
+ runner = if @validate_callbacks.method(:compile).arity == 0
89
+ @validate_callbacks.compile
90
+ else # ActiveRecord >= 7.1
91
+ @validate_callbacks.compile(nil)
92
+ end
54
93
  env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
55
94
  if runner.respond_to?(:call) # ActiveRecord < 5.1
56
95
  runner.call(env)
@@ -69,10 +108,10 @@ module ActiveRecord::Import #:nodoc:
69
108
  runner.invoke_before(env)
70
109
  runner.invoke_after(env)
71
110
  end
72
- elsif validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
73
- model.instance_eval validate_callbacks.compile
111
+ elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
112
+ model.instance_eval @validate_callbacks.compile
74
113
  else # ActiveRecord 3.x
75
- model.instance_eval validate_callbacks.compile(nil, model)
114
+ model.instance_eval @validate_callbacks.compile(nil, model)
76
115
  end
77
116
  end
78
117
 
@@ -130,7 +169,7 @@ class ActiveRecord::Associations::CollectionAssociation
130
169
  m.public_send "#{reflection.type}=", owner.class.name if reflection.type
131
170
  end
132
171
 
133
- return model_klass.bulk_import column_names, models, options
172
+ model_klass.bulk_import column_names, models, options
134
173
 
135
174
  # supports array of hash objects
136
175
  elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
@@ -169,11 +208,11 @@ class ActiveRecord::Associations::CollectionAssociation
169
208
  end
170
209
  end
171
210
 
172
- return model_klass.bulk_import column_names, array_of_attributes, options
211
+ model_klass.bulk_import column_names, array_of_attributes, options
173
212
 
174
213
  # supports empty array
175
214
  elsif args.last.is_a?( Array ) && args.last.empty?
176
- return ActiveRecord::Import::Result.new([], 0, [])
215
+ ActiveRecord::Import::Result.new([], 0, [])
177
216
 
178
217
  # supports 2-element array and array
179
218
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
@@ -204,7 +243,7 @@ class ActiveRecord::Associations::CollectionAssociation
204
243
  end
205
244
  end
206
245
 
207
- return model_klass.bulk_import column_names, array_of_attributes, options
246
+ model_klass.bulk_import column_names, array_of_attributes, options
208
247
  else
209
248
  raise ArgumentError, "Invalid arguments!"
210
249
  end
@@ -212,16 +251,17 @@ class ActiveRecord::Associations::CollectionAssociation
212
251
  alias import bulk_import unless respond_to? :import
213
252
  end
214
253
 
254
+ module ActiveRecord::Import::Connection
255
+ def establish_connection(args = nil)
256
+ conn = super(args)
257
+ ActiveRecord::Import.load_from_connection_pool connection_pool
258
+ conn
259
+ end
260
+ end
261
+
215
262
  class ActiveRecord::Base
216
263
  class << self
217
- def establish_connection_with_activerecord_import(*args)
218
- conn = establish_connection_without_activerecord_import(*args)
219
- ActiveRecord::Import.load_from_connection_pool connection_pool
220
- conn
221
- end
222
-
223
- alias establish_connection_without_activerecord_import establish_connection
224
- alias establish_connection establish_connection_with_activerecord_import
264
+ prepend ActiveRecord::Import::Connection
225
265
 
226
266
  # Returns true if the current database connection adapter
227
267
  # supports import functionality, otherwise returns false.
@@ -298,8 +338,8 @@ class ActiveRecord::Base
298
338
  # recursive import. For database adapters that normally support
299
339
  # setting primary keys on imported objects, this option prevents
300
340
  # that from occurring.
301
- # * +on_duplicate_key_update+ - an Array or Hash, tells import to
302
- # use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
341
+ # * +on_duplicate_key_update+ - :all, an Array, or Hash, tells import to
342
+ # use MySQL's ON DUPLICATE KEY UPDATE or Postgres/SQLite ON CONFLICT
303
343
  # DO UPDATE ability. See On Duplicate Key Update below.
304
344
  # * +synchronize+ - an array of ActiveRecord instances for the model
305
345
  # that you are currently importing data into. This synchronizes
@@ -358,7 +398,15 @@ class ActiveRecord::Base
358
398
  #
359
399
  # == On Duplicate Key Update (MySQL)
360
400
  #
361
- # The :on_duplicate_key_update option can be either an Array or a Hash.
401
+ # The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
402
+ #
403
+ # ==== Using :all
404
+ #
405
+ # The :on_duplicate_key_update option can be set to :all. All columns
406
+ # other than the primary key are updated. If a list of column names is
407
+ # supplied, only those columns will be updated. Below is an example:
408
+ #
409
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
362
410
  #
363
411
  # ==== Using an Array
364
412
  #
@@ -377,11 +425,19 @@ class ActiveRecord::Base
377
425
  #
378
426
  # BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
379
427
  #
380
- # == On Duplicate Key Update (Postgres 9.5+)
428
+ # == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
381
429
  #
382
- # The :on_duplicate_key_update option can be an Array or a Hash with up to
430
+ # The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
383
431
  # three attributes, :conflict_target (and optionally :index_predicate) or
384
- # :constraint_name, and :columns.
432
+ # :constraint_name (Postgres), and :columns.
433
+ #
434
+ # ==== Using :all
435
+ #
436
+ # The :on_duplicate_key_update option can be set to :all. All columns
437
+ # other than the primary key are updated. If a list of column names is
438
+ # supplied, only those columns will be updated. Below is an example:
439
+ #
440
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
385
441
  #
386
442
  # ==== Using an Array
387
443
  #
@@ -439,7 +495,15 @@ class ActiveRecord::Base
439
495
  #
440
496
  # ===== :columns
441
497
  #
442
- # The :columns attribute can be either an Array or a Hash.
498
+ # The :columns attribute can be either :all, an Array, or a Hash.
499
+ #
500
+ # ===== Using :all
501
+ #
502
+ # The :columns attribute can be :all. All columns other than the primary key will be updated.
503
+ # If a list of column names is supplied, only those columns will be updated.
504
+ # Below is an example:
505
+ #
506
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
443
507
  #
444
508
  # ===== Using an Array
445
509
  #
@@ -474,7 +538,7 @@ class ActiveRecord::Base
474
538
  import_helper(*args)
475
539
  end
476
540
  end
477
- alias import bulk_import unless respond_to? :import
541
+ alias import bulk_import unless ActiveRecord::Base.respond_to? :import
478
542
 
479
543
  # Imports a collection of values if all values are valid. Import fails at the
480
544
  # first encountered validation error and raises ActiveRecord::RecordInvalid
@@ -486,27 +550,17 @@ class ActiveRecord::Base
486
550
 
487
551
  bulk_import(*args, options)
488
552
  end
489
- alias import! bulk_import! unless respond_to? :import!
553
+ alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
490
554
 
491
555
  def import_helper( *args )
492
- options = { validate: true, timestamps: true }
556
+ options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
493
557
  options.merge!( args.pop ) if args.last.is_a? Hash
494
558
  # making sure that current model's primary key is used
495
559
  options[:primary_key] = primary_key
496
- options[:locking_column] = locking_column if attribute_names.include?(locking_column)
497
-
498
- # Don't modify incoming arguments
499
- on_duplicate_key_update = options[:on_duplicate_key_update]
500
- if on_duplicate_key_update && on_duplicate_key_update.duplicable?
501
- options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
502
- on_duplicate_key_update.each { |k, v| on_duplicate_key_update[k] = v.dup if v.duplicable? }
503
- else
504
- on_duplicate_key_update.dup
505
- end
506
- end
560
+ options[:locking_column] = locking_column if locking_enabled?
507
561
 
508
562
  is_validating = options[:validate_with_context].present? ? true : options[:validate]
509
- validator = ActiveRecord::Import::Validator.new(options)
563
+ validator = ActiveRecord::Import::Validator.new(self, options)
510
564
 
511
565
  # assume array of model objects
512
566
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
@@ -522,17 +576,20 @@ class ActiveRecord::Base
522
576
  end
523
577
  end
524
578
 
525
- if models.first.id.nil? && column_names.include?(primary_key) && columns_hash[primary_key].type == :uuid
526
- column_names.delete(primary_key)
579
+ if models.first.id.nil?
580
+ Array(primary_key).each do |c|
581
+ if column_names.include?(c) && schema_columns_hash[c].type == :uuid
582
+ column_names.delete(c)
583
+ end
584
+ end
527
585
  end
528
586
 
529
- default_values = column_defaults
530
- stored_attrs = respond_to?(:stored_attributes) ? stored_attributes : {}
531
- serialized_attrs = if defined?(ActiveRecord::Type::Serialized)
532
- attrs = column_names.select { |c| type_for_attribute(c.to_s).class == ActiveRecord::Type::Serialized }
533
- Hash[attrs.map { |a| [a, nil] }]
534
- else
535
- serialized_attributes
587
+ update_attrs = if record_timestamps && options[:timestamps]
588
+ if respond_to?(:timestamp_attributes_for_update, true)
589
+ send(:timestamp_attributes_for_update).map(&:to_sym)
590
+ else
591
+ allocate.send(:timestamp_attributes_for_update_in_model)
592
+ end
536
593
  end
537
594
 
538
595
  array_of_attributes = []
@@ -548,12 +605,12 @@ class ActiveRecord::Base
548
605
  end
549
606
 
550
607
  array_of_attributes << column_names.map do |name|
551
- if stored_attrs.key?(name.to_sym) ||
552
- serialized_attrs.key?(name) ||
553
- default_values.key?(name.to_s)
554
- model.read_attribute(name.to_s)
608
+ if model.persisted? &&
609
+ update_attrs && update_attrs.include?(name.to_sym) &&
610
+ !model.send("#{name}_changed?")
611
+ nil
555
612
  else
556
- model.read_attribute_before_type_cast(name.to_s)
613
+ model.read_attribute(name.to_s)
557
614
  end
558
615
  end
559
616
  end
@@ -580,7 +637,7 @@ class ActiveRecord::Base
580
637
  end
581
638
  # supports empty array
582
639
  elsif args.last.is_a?( Array ) && args.last.empty?
583
- return ActiveRecord::Import::Result.new([], 0, [])
640
+ return ActiveRecord::Import::Result.new([], 0, [], [])
584
641
  # supports 2-element array and array
585
642
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
586
643
 
@@ -611,17 +668,44 @@ class ActiveRecord::Base
611
668
  array_of_attributes.each { |a| a.concat(new_fields) }
612
669
  end
613
670
 
671
+ # Don't modify incoming arguments
672
+ on_duplicate_key_update = options[:on_duplicate_key_update]
673
+ if on_duplicate_key_update
674
+ updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
675
+ options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
676
+ on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
677
+ duped_options[k] = if k == :columns && v == :all
678
+ updatable_columns
679
+ elsif v.duplicable?
680
+ v.dup
681
+ else
682
+ v
683
+ end
684
+ end
685
+ elsif on_duplicate_key_update == :all
686
+ updatable_columns
687
+ elsif on_duplicate_key_update.duplicable?
688
+ on_duplicate_key_update.dup
689
+ else
690
+ on_duplicate_key_update
691
+ end
692
+ end
693
+
614
694
  timestamps = {}
615
695
 
616
696
  # record timestamps unless disabled in ActiveRecord::Base
617
- if record_timestamps && options.delete( :timestamps )
697
+ if record_timestamps && options[:timestamps]
618
698
  timestamps = add_special_rails_stamps column_names, array_of_attributes, options
619
699
  end
620
700
 
621
701
  return_obj = if is_validating
622
702
  import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
623
703
  if models
624
- models.each { |m| failed_instances << m if m.errors.any? }
704
+ models.each_with_index do |m, i|
705
+ next unless m.errors.any?
706
+
707
+ failed_instances << (options[:track_validation_failures] ? [i, m] : m)
708
+ end
625
709
  else
626
710
  # create instances for each of our column/value sets
627
711
  arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
@@ -629,14 +713,18 @@ class ActiveRecord::Base
629
713
  # keep track of the instance and the position it is currently at. if this fails
630
714
  # validation we'll use the index to remove it from the array_of_attributes
631
715
  arr.each_with_index do |hsh, i|
632
- model = new
633
- hsh.each_pair { |k, v| model[k] = v }
716
+ # utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
717
+ model = new do |m|
718
+ hsh.each_pair { |k, v| m[k] = v }
719
+ end
720
+
634
721
  next if validator.valid_model?(model)
635
722
  raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
723
+
636
724
  array_of_attributes[i] = nil
637
725
  failure = model.dup
638
726
  failure.errors.send(:initialize_dup, model.errors)
639
- failed_instances << failure
727
+ failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
640
728
  end
641
729
  array_of_attributes.compact!
642
730
  end
@@ -646,7 +734,7 @@ class ActiveRecord::Base
646
734
  end
647
735
 
648
736
  if options[:synchronize]
649
- sync_keys = options[:synchronize_keys] || [primary_key]
737
+ sync_keys = options[:synchronize_keys] || Array(primary_key)
650
738
  synchronize( options[:synchronize], sync_keys)
651
739
  end
652
740
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
@@ -656,7 +744,10 @@ class ActiveRecord::Base
656
744
  set_attributes_and_mark_clean(models, return_obj, timestamps, options)
657
745
 
658
746
  # if there are auto-save associations on the models we imported that are new, import them as well
659
- import_associations(models, options.dup) if options[:recursive]
747
+ if options[:recursive]
748
+ options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
749
+ import_associations(models, options.dup.merge(validate: false))
750
+ end
660
751
  end
661
752
 
662
753
  return_obj
@@ -691,27 +782,29 @@ class ActiveRecord::Base
691
782
  def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
692
783
  return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
693
784
 
694
- column_names = column_names.map(&:to_sym)
785
+ column_names = column_names.map do |name|
786
+ original_name = attribute_alias?(name) ? attribute_alias(name) : name
787
+ original_name.to_sym
788
+ end
695
789
  scope_columns, scope_values = scope_attributes.to_a.transpose
696
790
 
697
791
  unless scope_columns.blank?
698
792
  scope_columns.zip(scope_values).each do |name, value|
699
793
  name_as_sym = name.to_sym
700
- next if column_names.include?(name_as_sym)
701
-
702
- is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
703
- value = Array(value).first if is_sti
704
-
794
+ next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
705
795
  column_names << name_as_sym
706
796
  array_of_attributes.each { |attrs| attrs << value }
707
797
  end
708
798
  end
709
799
 
710
- columns = column_names.each_with_index.map do |name, i|
711
- column = columns_hash[name.to_s]
800
+ if finder_needs_type_condition? && !column_names.include?(inheritance_column.to_sym)
801
+ column_names << inheritance_column.to_sym
802
+ array_of_attributes.each { |attrs| attrs << sti_name }
803
+ end
712
804
 
805
+ columns = column_names.each_with_index.map do |name, i|
806
+ column = schema_columns_hash[name.to_s]
713
807
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
714
-
715
808
  column
716
809
  end
717
810
 
@@ -727,17 +820,29 @@ class ActiveRecord::Base
727
820
  if supports_import?
728
821
  # generate the sql
729
822
  post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
823
+ import_size = values_sql.size
824
+
825
+ batch_size = options[:batch_size] || import_size
826
+ run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
827
+ progress_proc = options[:batch_progress]
828
+ current_batch = 0
829
+ batches = (import_size / batch_size.to_f).ceil
730
830
 
731
- batch_size = options[:batch_size] || values_sql.size
732
831
  values_sql.each_slice(batch_size) do |batch_values|
832
+ batch_started_at = Time.now.to_i
833
+
733
834
  # perform the inserts
734
835
  result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
735
836
  batch_values,
736
837
  options,
737
- "#{model_name} Create Many Without Validations Or Callbacks" )
838
+ "#{model_name} Create Many" )
839
+
738
840
  number_inserted += result.num_inserts
739
841
  ids += result.ids
740
842
  results += result.results
843
+ current_batch += 1
844
+
845
+ progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
741
846
  end
742
847
  else
743
848
  transaction(requires_new: true) do
@@ -752,6 +857,15 @@ class ActiveRecord::Base
752
857
 
753
858
  private
754
859
 
860
+ def associated_options(options, associated_class)
861
+ return options unless options.key?(:recursive_on_duplicate_key_update)
862
+
863
+ table_name = associated_class.arel_table.name.to_sym
864
+ options.merge(
865
+ on_duplicate_key_update: options[:recursive_on_duplicate_key_update][table_name]
866
+ )
867
+ end
868
+
755
869
  def set_attributes_and_mark_clean(models, import_result, timestamps, options)
756
870
  return if models.nil?
757
871
  models -= import_result.failed_instances
@@ -763,29 +877,56 @@ class ActiveRecord::Base
763
877
  model.id = id
764
878
 
765
879
  timestamps.each do |attr, value|
766
- model.send(attr + "=", value)
880
+ model.send("#{attr}=", value) if model.send(attr).nil?
767
881
  end
768
882
  end
769
883
  end
770
884
 
771
- if models.size == import_result.results.size
772
- columns = Array(options[:returning])
773
- single_column = "#{columns.first}=" if columns.size == 1
774
- import_result.results.each_with_index do |result, index|
885
+ deserialize_value = lambda do |column, value|
886
+ column = schema_columns_hash[column]
887
+ return value unless column
888
+ if respond_to?(:type_caster)
889
+ type = type_for_attribute(column.name)
890
+ type.deserialize(value)
891
+ elsif column.respond_to?(:type_cast_from_database)
892
+ column.type_cast_from_database(value)
893
+ else
894
+ value
895
+ end
896
+ end
897
+
898
+ set_value = lambda do |model, column, value|
899
+ val = deserialize_value.call(column, value)
900
+ if model.attribute_names.include?(column)
901
+ model.send("#{column}=", val)
902
+ else
903
+ attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
904
+ model.instance_variable_set(:@attributes, attributes)
905
+ end
906
+ end
907
+
908
+ columns = Array(options[:returning_columns])
909
+ results = Array(import_result.results)
910
+ if models.size == results.size
911
+ single_column = columns.first if columns.size == 1
912
+ results.each_with_index do |result, index|
775
913
  model = models[index]
776
914
 
777
915
  if single_column
778
- model.send(single_column, result)
916
+ set_value.call(model, single_column, result)
779
917
  else
780
918
  columns.each_with_index do |column, col_index|
781
- model.send("#{column}=", result[col_index])
919
+ set_value.call(model, column, result[col_index])
782
920
  end
783
921
  end
784
922
  end
785
923
  end
786
924
 
787
925
  models.each do |model|
788
- if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
926
+ if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
927
+ model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
928
+ model.changes_applied
929
+ elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
789
930
  model.clear_changes_information
790
931
  else # Rails 3.2
791
932
  model.instance_variable_get(:@changed_attributes).clear
@@ -796,16 +937,22 @@ class ActiveRecord::Base
796
937
 
797
938
  # Sync belongs_to association ids with foreign key field
798
939
  def load_association_ids(model)
940
+ changed_columns = model.changed
799
941
  association_reflections = model.class.reflect_on_all_associations(:belongs_to)
800
942
  association_reflections.each do |association_reflection|
801
- column_name = association_reflection.foreign_key
802
943
  next if association_reflection.options[:polymorphic]
803
- association = model.association(association_reflection.name)
804
- association = association.target
805
- next if association.blank? || model.public_send(column_name).present?
806
944
 
807
- association_primary_key = association_reflection.association_primary_key
808
- model.public_send("#{column_name}=", association.send(association_primary_key))
945
+ column_names = Array(association_reflection.foreign_key).map(&:to_s)
946
+ column_names.each_with_index do |column_name, column_index|
947
+ next if changed_columns.include?(column_name)
948
+
949
+ association = model.association(association_reflection.name)
950
+ association = association.target
951
+ next if association.blank? || model.public_send(column_name).present?
952
+
953
+ association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
954
+ model.public_send("#{column_name}=", association.send(association_primary_key))
955
+ end
809
956
  end
810
957
  end
811
958
 
@@ -818,16 +965,30 @@ class ActiveRecord::Base
818
965
  associated_objects_by_class = {}
819
966
  models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
820
967
 
821
- # :on_duplicate_key_update not supported for associations
822
- options.delete(:on_duplicate_key_update)
968
+ # :on_duplicate_key_update only supported for all fields
969
+ options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
970
+ # :returning not supported for associations
971
+ options.delete(:returning)
823
972
 
824
973
  associated_objects_by_class.each_value do |associations|
825
974
  associations.each_value do |associated_records|
826
- associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
975
+ next if associated_records.empty?
976
+
977
+ associated_class = associated_records.first.class
978
+ associated_class.bulk_import(associated_records,
979
+ associated_options(options, associated_class))
827
980
  end
828
981
  end
829
982
  end
830
983
 
984
+ def schema_columns_hash
985
+ if respond_to?(:ignored_columns) && ignored_columns.any?
986
+ connection.schema_cache.columns_hash(table_name)
987
+ else
988
+ columns_hash
989
+ end
990
+ end
991
+
831
992
  # We are eventually going to call Class.import <objects> so we build up a hash
832
993
  # of class => objects to import.
833
994
  def find_associated_objects_for_import(associated_objects_by_class, model)
@@ -848,10 +1009,18 @@ class ActiveRecord::Base
848
1009
 
849
1010
  changed_objects = association.select { |a| a.new_record? || a.changed? }
850
1011
  changed_objects.each do |child|
851
- child.public_send("#{association_reflection.foreign_key}=", model.id)
1012
+ Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
1013
+ child.public_send("#{column}=", Array(model.id)[index])
1014
+ end
1015
+
852
1016
  # For polymorphic associations
1017
+ association_name = if model.class.respond_to?(:polymorphic_name)
1018
+ model.class.polymorphic_name
1019
+ else
1020
+ model.class.base_class
1021
+ end
853
1022
  association_reflection.type.try do |type|
854
- child.public_send("#{type}=", model.class.base_class.name)
1023
+ child.public_send("#{type}=", association_name)
855
1024
  end
856
1025
  end
857
1026
  associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
@@ -871,14 +1040,14 @@ class ActiveRecord::Base
871
1040
  column = columns[j]
872
1041
 
873
1042
  # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
874
- if val.nil? && column.name == primary_key && !sequence_name.blank?
1043
+ if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
875
1044
  connection_memo.next_value_for_sequence(sequence_name)
876
1045
  elsif val.respond_to?(:to_sql)
877
1046
  "(#{val.to_sql})"
878
1047
  elsif column
879
1048
  if respond_to?(:type_caster) # Rails 5.0 and higher
880
1049
  type = type_for_attribute(column.name)
881
- val = type.type == :boolean ? type.cast(val) : type.serialize(val)
1050
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
882
1051
  connection_memo.quote(val)
883
1052
  elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
884
1053
  connection_memo.quote(column.type_cast_from_user(val), column)
@@ -887,9 +1056,11 @@ class ActiveRecord::Base
887
1056
  val = serialized_attributes[column.name].dump(val)
888
1057
  end
889
1058
  # Fixes #443 to support binary (i.e. bytea) columns on PG
890
- val = column.type_cast(val) unless column.type.to_sym == :binary
1059
+ val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
891
1060
  connection_memo.quote(val, column)
892
1061
  end
1062
+ else
1063
+ raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
893
1064
  end
894
1065
  end
895
1066
  "(#{my_values.join(',')})"
@@ -904,13 +1075,18 @@ class ActiveRecord::Base
904
1075
  timestamp_columns[:create] = timestamp_attributes_for_create_in_model
905
1076
  timestamp_columns[:update] = timestamp_attributes_for_update_in_model
906
1077
  else
907
- instance = new
1078
+ instance = allocate
908
1079
  timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
909
1080
  timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
910
1081
  end
911
1082
 
912
1083
  # use tz as set in ActiveRecord::Base
913
- timestamp = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
1084
+ default_timezone = if ActiveRecord.respond_to?(:default_timezone)
1085
+ ActiveRecord.default_timezone
1086
+ else
1087
+ ActiveRecord::Base.default_timezone
1088
+ end
1089
+ timestamp = default_timezone == :utc ? Time.now.utc : Time.now
914
1090
 
915
1091
  [:create, :update].each do |action|
916
1092
  timestamp_columns[action].each do |column|
@@ -920,7 +1096,7 @@ class ActiveRecord::Base
920
1096
  index = column_names.index(column) || column_names.index(column.to_sym)
921
1097
  if index
922
1098
  # replace every instance of the array of attributes with our value
923
- array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? || action == :update }
1099
+ array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
924
1100
  else
925
1101
  column_names << column
926
1102
  array_of_attributes.each { |arr| arr << timestamp }