activerecord-import 0.25.0 → 1.7.0

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