activerecord-import 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -0
  3. data/README.markdown +99 -32
  4. data/lib/activerecord-import/active_record/adapters/mysql2_proxy_adapter.rb +8 -0
  5. data/lib/activerecord-import/active_record/adapters/postgresql_proxy_adapter.rb +8 -0
  6. data/lib/activerecord-import/active_record/adapters/sqlite3_proxy_adapter.rb +8 -0
  7. data/lib/activerecord-import/active_record/adapters/trilogy_proxy_adapter.rb +8 -0
  8. data/lib/activerecord-import/adapters/abstract_adapter.rb +58 -0
  9. data/lib/activerecord-import/adapters/active_record_proxy_adapter.rb +19 -0
  10. data/lib/activerecord-import/adapters/mysql2_proxy_adapter.rb +9 -0
  11. data/lib/activerecord-import/adapters/mysql_adapter.rb +70 -13
  12. data/lib/activerecord-import/adapters/postgresql_adapter.rb +10 -52
  13. data/lib/activerecord-import/adapters/postgresql_proxy_adapter.rb +9 -0
  14. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -3
  15. data/lib/activerecord-import/adapters/sqlite3_proxy_adapter.rb +9 -0
  16. data/lib/activerecord-import/adapters/trilogy_proxy_adapter.rb +9 -0
  17. data/lib/activerecord-import/aliases.rb +42 -0
  18. data/lib/activerecord-import/base.rb +2 -46
  19. data/lib/activerecord-import/core.rb +62 -0
  20. data/lib/activerecord-import/import.rb +612 -369
  21. data/lib/activerecord-import/synchronize.rb +28 -16
  22. data/lib/activerecord-import/version.rb +1 -1
  23. data/lib/activerecord-import/without_aliases.rb +17 -0
  24. metadata +19 -211
  25. data/.github/workflows/test.yaml +0 -165
  26. data/.gitignore +0 -37
  27. data/.rubocop.yml +0 -118
  28. data/.rubocop_todo.yml +0 -30
  29. data/Brewfile +0 -5
  30. data/Dockerfile +0 -23
  31. data/Gemfile +0 -63
  32. data/Rakefile +0 -75
  33. data/activerecord-import.gemspec +0 -28
  34. data/benchmarks/README +0 -32
  35. data/benchmarks/benchmark.rb +0 -72
  36. data/benchmarks/lib/base.rb +0 -143
  37. data/benchmarks/lib/cli_parser.rb +0 -111
  38. data/benchmarks/lib/float.rb +0 -17
  39. data/benchmarks/lib/mysql2_benchmark.rb +0 -21
  40. data/benchmarks/lib/output_to_csv.rb +0 -21
  41. data/benchmarks/lib/output_to_html.rb +0 -66
  42. data/benchmarks/models/test_innodb.rb +0 -5
  43. data/benchmarks/models/test_memory.rb +0 -5
  44. data/benchmarks/models/test_myisam.rb +0 -5
  45. data/benchmarks/schema/mysql2_schema.rb +0 -18
  46. data/docker-compose.yml +0 -34
  47. data/gemfiles/5.2.gemfile +0 -4
  48. data/gemfiles/6.0.gemfile +0 -4
  49. data/gemfiles/6.1.gemfile +0 -4
  50. data/gemfiles/7.0.gemfile +0 -4
  51. data/gemfiles/7.1.gemfile +0 -3
  52. data/gemfiles/7.2.gemfile +0 -3
  53. data/gemfiles/8.0.gemfile +0 -3
  54. data/test/adapters/janus_mysql2.rb +0 -3
  55. data/test/adapters/janus_trilogy.rb +0 -11
  56. data/test/adapters/jdbcmysql.rb +0 -3
  57. data/test/adapters/jdbcpostgresql.rb +0 -3
  58. data/test/adapters/jdbcsqlite3.rb +0 -3
  59. data/test/adapters/makara_postgis.rb +0 -3
  60. data/test/adapters/mysql2.rb +0 -3
  61. data/test/adapters/mysql2_makara.rb +0 -3
  62. data/test/adapters/mysql2_proxy.rb +0 -3
  63. data/test/adapters/mysql2spatial.rb +0 -3
  64. data/test/adapters/postgis.rb +0 -3
  65. data/test/adapters/postgresql.rb +0 -3
  66. data/test/adapters/postgresql_makara.rb +0 -3
  67. data/test/adapters/postgresql_proxy.rb +0 -3
  68. data/test/adapters/seamless_database_pool.rb +0 -3
  69. data/test/adapters/spatialite.rb +0 -3
  70. data/test/adapters/sqlite3.rb +0 -3
  71. data/test/adapters/trilogy.rb +0 -11
  72. data/test/database.yml.sample +0 -73
  73. data/test/github/database.yml +0 -85
  74. data/test/import_test.rb +0 -970
  75. data/test/janus_mysql2/import_test.rb +0 -8
  76. data/test/janus_trilogy/import_test.rb +0 -7
  77. data/test/jdbcmysql/import_test.rb +0 -7
  78. data/test/jdbcpostgresql/import_test.rb +0 -6
  79. data/test/jdbcsqlite3/import_test.rb +0 -6
  80. data/test/makara_postgis/import_test.rb +0 -10
  81. data/test/models/account.rb +0 -5
  82. data/test/models/alarm.rb +0 -4
  83. data/test/models/animal.rb +0 -8
  84. data/test/models/author.rb +0 -9
  85. data/test/models/bike_maker.rb +0 -10
  86. data/test/models/book.rb +0 -18
  87. data/test/models/car.rb +0 -5
  88. data/test/models/card.rb +0 -5
  89. data/test/models/chapter.rb +0 -6
  90. data/test/models/composite_book.rb +0 -19
  91. data/test/models/composite_chapter.rb +0 -12
  92. data/test/models/customer.rb +0 -18
  93. data/test/models/deck.rb +0 -8
  94. data/test/models/dictionary.rb +0 -6
  95. data/test/models/discount.rb +0 -5
  96. data/test/models/end_note.rb +0 -6
  97. data/test/models/group.rb +0 -5
  98. data/test/models/order.rb +0 -17
  99. data/test/models/playing_card.rb +0 -4
  100. data/test/models/promotion.rb +0 -5
  101. data/test/models/question.rb +0 -5
  102. data/test/models/rule.rb +0 -5
  103. data/test/models/tag.rb +0 -12
  104. data/test/models/tag_alias.rb +0 -11
  105. data/test/models/topic.rb +0 -31
  106. data/test/models/user.rb +0 -5
  107. data/test/models/user_token.rb +0 -6
  108. data/test/models/vendor.rb +0 -9
  109. data/test/models/widget.rb +0 -33
  110. data/test/mysql2/import_test.rb +0 -7
  111. data/test/mysql2_makara/import_test.rb +0 -8
  112. data/test/mysql2_proxy/import_test.rb +0 -6
  113. data/test/mysqlspatial2/import_test.rb +0 -8
  114. data/test/postgis/import_test.rb +0 -10
  115. data/test/postgresql/import_test.rb +0 -6
  116. data/test/postgresql_proxy/import_test.rb +0 -6
  117. data/test/schema/generic_schema.rb +0 -230
  118. data/test/schema/jdbcpostgresql_schema.rb +0 -3
  119. data/test/schema/mysql2_schema.rb +0 -21
  120. data/test/schema/postgis_schema.rb +0 -3
  121. data/test/schema/postgresql_schema.rb +0 -94
  122. data/test/schema/sqlite3_schema.rb +0 -15
  123. data/test/schema/version.rb +0 -12
  124. data/test/sqlite3/import_test.rb +0 -6
  125. data/test/support/active_support/test_case_extensions.rb +0 -73
  126. data/test/support/assertions.rb +0 -75
  127. data/test/support/factories.rb +0 -66
  128. data/test/support/generate.rb +0 -31
  129. data/test/support/mysql/import_examples.rb +0 -97
  130. data/test/support/postgresql/import_examples.rb +0 -642
  131. data/test/support/shared_examples/on_duplicate_key_ignore.rb +0 -45
  132. data/test/support/shared_examples/on_duplicate_key_update.rb +0 -437
  133. data/test/support/shared_examples/recursive_import.rb +0 -352
  134. data/test/support/sqlite3/import_examples.rb +0 -232
  135. data/test/synchronize_test.rb +0 -43
  136. data/test/test_helper.rb +0 -91
  137. data/test/trilogy/import_test.rb +0 -7
  138. data/test/value_sets_bytes_parser_test.rb +0 -106
  139. data/test/value_sets_records_parser_test.rb +0 -34
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord::Import::ConnectionAdapters; end
4
4
 
5
5
  module ActiveRecord::Import # :nodoc:
6
- Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
6
+ Result = Struct.new(:failed_instances, :num_inserts, :ids, :results, :affected_rows)
7
7
 
8
8
  module ImportSupport # :nodoc:
9
9
  def supports_import? # :nodoc:
@@ -23,111 +23,84 @@ module ActiveRecord::Import # :nodoc:
23
23
  end
24
24
  end
25
25
 
26
- class Validator
27
- def initialize(klass, options = {})
28
- @options = options
29
- @validator_class = klass
30
- init_validations(klass)
26
+ # Skips uniqueness validations while a model is being validated for an import
27
+ # that doesn't validate uniqueness, including the validations Rails runs on
28
+ # the model's associated records.
29
+ module SkipUniquenessValidation # :nodoc:
30
+ def validate_each(record, attribute, value)
31
+ super unless ActiveRecord::Import::Validator.skip_uniqueness?
31
32
  end
33
+ end
32
34
 
33
- def init_validations(klass)
34
- @validate_callbacks = klass._validate_callbacks.dup
35
+ # Validates the presence of a belongs_to association on a model being
36
+ # validated for an import by checking its foreign key, so the associated
37
+ # record isn't loaded for every model.
38
+ module ForeignKeyPresenceValidation # :nodoc:
39
+ def validate(record)
40
+ validator = ActiveRecord::Import::Validator.foreign_key_presence_validator(self, record)
41
+ return super unless validator
35
42
 
36
- @validate_callbacks.each_with_index do |callback, i|
37
- filter = callback.respond_to?(:raw_filter) ? callback.raw_filter : callback.filter
38
- next unless filter.class.name =~ /Validations::PresenceValidator/ ||
39
- (!@options[:validate_uniqueness] &&
40
- filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
43
+ ActiveModel::EachValidator.instance_method(:validate).bind(validator).call(record)
44
+ end
45
+ end
41
46
 
42
- callback = callback.dup
43
- filter = filter.dup
44
- attrs = filter.instance_variable_get(:@attributes).dup
47
+ class Validator
48
+ # Holds [validator, model] for the current thread while valid_model? runs.
49
+ CURRENT = :activerecord_import_validation
45
50
 
46
- if filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
47
- attrs = []
48
- else
49
- associations = klass.reflect_on_all_associations(:belongs_to)
50
- associations.each do |assoc|
51
- if (index = attrs.index(assoc.name))
52
- key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
53
- attrs[index] = key unless attrs.include?(key)
54
- end
55
- end
56
- end
51
+ attr_reader :options
57
52
 
58
- filter.instance_variable_set(:@attributes, attrs.flatten)
53
+ def self.skip_uniqueness?
54
+ validator, = Thread.current[CURRENT]
55
+ validator ? !validator.options[:validate_uniqueness] : false
56
+ end
59
57
 
60
- if @validate_callbacks.respond_to?(:chain, true)
61
- @validate_callbacks.send(:chain).tap do |chain|
62
- callback.instance_variable_set(:@filter, filter)
63
- callback.instance_variable_set(:@compiled, nil)
64
- chain[i] = callback
65
- end
66
- else
67
- callback.raw_filter = filter
68
- callback.filter = callback.send(:_compile_filter, filter)
69
- @validate_callbacks[i] = callback
70
- end
71
- end
58
+ def self.foreign_key_presence_validator(presence_validator, record)
59
+ validator, model = Thread.current[CURRENT]
60
+ validator.foreign_key_presence_validator(presence_validator, record.class) if model.equal?(record)
61
+ end
62
+
63
+ def initialize(options = {})
64
+ @options = options
65
+ @foreign_key_presence_validators = {}
72
66
  end
73
67
 
74
68
  def valid_model?(model)
75
- init_validations(model.class) unless model.instance_of?(@validator_class)
76
-
77
- validation_context = @options[:validate_with_context]
78
- validation_context ||= (model.new_record? ? :create : :update)
79
- current_context = model.send(:validation_context)
80
-
81
- begin
82
- model.send(:validation_context=, validation_context)
83
- model.errors.clear
84
-
85
- model.run_callbacks(:validation) do
86
- if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
87
- runner = if @validate_callbacks.method(:compile).arity == 0
88
- @validate_callbacks.compile
89
- else # ActiveRecord >= 7.1
90
- @validate_callbacks.compile(nil)
91
- end
92
- env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
93
- if runner.respond_to?(:call) # ActiveRecord < 5.1
94
- runner.call(env)
95
- else # ActiveRecord >= 5.1
96
- # Note that this is a gross simplification of ActiveSupport::Callbacks#run_callbacks.
97
- # It's technically possible for there to exist an "around" callback in the
98
- # :validate chain, but this would be an aberration, since Rails doesn't define
99
- # "around_validate". Still, rather than silently ignoring such callbacks, we
100
- # explicitly raise a RuntimeError, since activerecord-import was asked to perform
101
- # validations and it's unable to do so.
102
- #
103
- # The alternative here would be to copy-and-paste the bulk of the
104
- # ActiveSupport::Callbacks#run_callbacks method, which is undesirable if there's
105
- # no real-world use case for it.
106
- raise "The :validate callback chain contains an 'around' callback, which is unsupported" unless runner.final?
107
- runner.invoke_before(env)
108
- # Ensure a truthy value is returned. ActiveRecord < 7.2 always returned an array.
109
- runner.invoke_after(env) || []
110
- end
111
- elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
112
- model.instance_eval @validate_callbacks.compile
113
- else # ActiveRecord 3.x
114
- model.instance_eval @validate_callbacks.compile(nil, model)
115
- end
69
+ current = Thread.current[CURRENT]
70
+ Thread.current[CURRENT] = [self, model]
71
+ model.valid?(@options[:validate_with_context])
72
+ ensure
73
+ Thread.current[CURRENT] = current
74
+ end
75
+
76
+ # Returns a copy of +presence_validator+ that validates the foreign keys of
77
+ # any belongs_to associations it validates, or nil if there are none.
78
+ def foreign_key_presence_validator(presence_validator, klass)
79
+ key = [presence_validator, klass]
80
+ return @foreign_key_presence_validators[key] if @foreign_key_presence_validators.key?(key)
81
+
82
+ attrs = presence_validator.attributes.dup
83
+ klass.reflect_on_all_associations(:belongs_to).each do |assoc|
84
+ if (index = attrs.index(assoc.name))
85
+ foreign_key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
86
+ attrs[index] = foreign_key unless attrs.include?(foreign_key)
116
87
  end
88
+ end
117
89
 
118
- model.errors.empty?
119
- ensure
120
- model.send(:validation_context=, current_context)
90
+ @foreign_key_presence_validators[key] = if attrs != presence_validator.attributes
91
+ presence_validator.dup.tap { |validator| validator.instance_variable_set(:@attributes, attrs.flatten) }
121
92
  end
122
93
  end
123
94
  end
124
95
  end
125
96
 
97
+ ActiveRecord::Validations::UniquenessValidator.prepend(ActiveRecord::Import::SkipUniquenessValidation)
98
+ ActiveModel::Validations::PresenceValidator.prepend(ActiveRecord::Import::ForeignKeyPresenceValidation)
99
+
126
100
  class ActiveRecord::Associations::CollectionProxy
127
101
  def bulk_import(*args, &block)
128
102
  @association.bulk_import(*args, &block)
129
103
  end
130
- alias import bulk_import unless respond_to? :import
131
104
  end
132
105
 
133
106
  class ActiveRecord::Associations::CollectionAssociation
@@ -139,9 +112,10 @@ class ActiveRecord::Associations::CollectionAssociation
139
112
  options = args.last.is_a?(Hash) ? args.pop : {}
140
113
 
141
114
  model_klass = reflection.klass
115
+ args = model_klass.send(:arrays_from_array_like_import_arguments, args)
142
116
  symbolized_foreign_key = reflection.foreign_key.to_sym
143
117
 
144
- symbolized_column_names = if model_klass.connection.respond_to?(:supports_virtual_columns?) && model_klass.connection.supports_virtual_columns?
118
+ symbolized_column_names = if model_klass.connection_pool.with_connection(&:supports_virtual_columns?)
145
119
  model_klass.columns.reject(&:virtual?).map { |c| c.name.to_sym }
146
120
  else
147
121
  model_klass.column_names.map(&:to_sym)
@@ -149,12 +123,14 @@ class ActiveRecord::Associations::CollectionAssociation
149
123
 
150
124
  owner_primary_key = reflection.active_record_primary_key.to_sym
151
125
  owner_primary_key_value = owner.send(owner_primary_key)
126
+ owner_polymorphic_name = owner.class.polymorphic_name if reflection.type
152
127
 
153
128
  # assume array of model objects
154
129
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
155
130
  if args.length == 2
156
131
  models = args.last
157
132
  column_names = args.first.dup
133
+ symbolized_column_names = column_names.map(&:to_sym)
158
134
  else
159
135
  models = args.first
160
136
  column_names = symbolized_column_names
@@ -164,9 +140,13 @@ class ActiveRecord::Associations::CollectionAssociation
164
140
  column_names << symbolized_foreign_key
165
141
  end
166
142
 
143
+ if reflection.type && !symbolized_column_names.include?(reflection.type.to_sym)
144
+ column_names << reflection.type.to_sym
145
+ end
146
+
167
147
  models.each do |m|
168
148
  m.public_send "#{symbolized_foreign_key}=", owner_primary_key_value
169
- m.public_send "#{reflection.type}=", owner.class.name if reflection.type
149
+ m.public_send "#{reflection.type}=", owner_polymorphic_name if reflection.type
170
150
  end
171
151
 
172
152
  model_klass.bulk_import column_names, models, options
@@ -183,6 +163,7 @@ class ActiveRecord::Associations::CollectionAssociation
183
163
  allow_extra_hash_keys = false
184
164
  end
185
165
 
166
+ required_column_names = column_names.dup
186
167
  symbolized_column_names = column_names.map(&:to_sym)
187
168
  unless symbolized_column_names.include?(symbolized_foreign_key)
188
169
  column_names << symbolized_foreign_key
@@ -193,15 +174,16 @@ class ActiveRecord::Associations::CollectionAssociation
193
174
  end
194
175
 
195
176
  array_of_attributes = array_of_hashes.map do |h|
196
- error_message = model_klass.send(:validate_hash_import, h, symbolized_column_names, allow_extra_hash_keys)
177
+ error_message = model_klass.send(:validate_hash_import, h, required_column_names, allow_extra_hash_keys)
197
178
 
198
179
  raise ArgumentError, error_message if error_message
199
180
 
200
181
  column_names.map do |key|
201
- if key == symbolized_foreign_key
182
+ symbolized_key = key.to_sym
183
+ if symbolized_key == symbolized_foreign_key
202
184
  owner_primary_key_value
203
- elsif reflection.type && key == reflection.type.to_sym
204
- owner.class.name
185
+ elsif reflection.type && symbolized_key == reflection.type.to_sym
186
+ owner_polymorphic_name
205
187
  else
206
188
  h[key]
207
189
  end
@@ -236,51 +218,70 @@ class ActiveRecord::Associations::CollectionAssociation
236
218
  symbolized_type = reflection.type.to_sym
237
219
  if symbolized_column_names.include?(symbolized_type)
238
220
  index = symbolized_column_names.index(symbolized_type)
239
- array_of_attributes.each { |attrs| attrs[index] = owner.class.name }
221
+ array_of_attributes.each { |attrs| attrs[index] = owner_polymorphic_name }
240
222
  else
241
223
  column_names << symbolized_type
242
- array_of_attributes.each { |attrs| attrs << owner.class.name }
224
+ array_of_attributes.each { |attrs| attrs << owner_polymorphic_name }
243
225
  end
244
226
  end
245
227
 
246
228
  model_klass.bulk_import column_names, array_of_attributes, options
247
229
  else
248
- raise ArgumentError, "Invalid arguments!"
230
+ raise ArgumentError, model_klass.send(:invalid_import_arguments_message, args)
249
231
  end
250
232
  end
251
- alias import bulk_import unless respond_to? :import
252
233
  end
253
234
 
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
235
+ module ActiveRecord::Import::ConnectionHandler
236
+ def establish_connection(*args, **kwargs, &block)
237
+ pool = super(*args, **kwargs, &block)
238
+ ActiveRecord::Import.load_from_connection_pool pool
239
+ pool
259
240
  end
260
241
  end
261
242
 
243
+ class ActiveRecord::ConnectionAdapters::ConnectionHandler
244
+ prepend ActiveRecord::Import::ConnectionHandler
245
+ end
246
+
262
247
  class ActiveRecord::Base
263
248
  class << self
264
- prepend ActiveRecord::Import::Connection
265
-
266
249
  # Returns true if the current database connection adapter
267
250
  # supports import functionality, otherwise returns false.
268
- def supports_import?(*args)
269
- connection.respond_to?(:supports_import?) && connection.supports_import?(*args)
251
+ def supports_import?(conn = nil)
252
+ if conn
253
+ conn.respond_to?(:supports_import?) && conn.supports_import?
254
+ else
255
+ connection_pool.with_connection do |conn|
256
+ conn.respond_to?(:supports_import?) && conn.supports_import?
257
+ end
258
+ end
270
259
  end
271
260
 
272
261
  # Returns true if the current database connection adapter
273
262
  # supports on duplicate key update functionality, otherwise
274
263
  # returns false.
275
- def supports_on_duplicate_key_update?
276
- connection.respond_to?(:supports_on_duplicate_key_update?) && connection.supports_on_duplicate_key_update?
264
+ def supports_on_duplicate_key_update?(conn = nil)
265
+ if conn
266
+ conn.respond_to?(:supports_on_duplicate_key_update?) && conn.supports_on_duplicate_key_update?
267
+ else
268
+ connection_pool.with_connection do |conn|
269
+ conn.respond_to?(:supports_on_duplicate_key_update?) && conn.supports_on_duplicate_key_update?
270
+ end
271
+ end
277
272
  end
278
273
 
279
274
  # returns true if the current database connection adapter
280
275
  # supports setting the primary key of bulk imported models, otherwise
281
276
  # returns false
282
- def supports_setting_primary_key_of_imported_objects?
283
- connection.respond_to?(:supports_setting_primary_key_of_imported_objects?) && connection.supports_setting_primary_key_of_imported_objects?
277
+ def supports_setting_primary_key_of_imported_objects?(conn = nil)
278
+ if conn
279
+ conn.respond_to?(:supports_setting_primary_key_of_imported_objects?) && conn.supports_setting_primary_key_of_imported_objects?
280
+ else
281
+ connection_pool.with_connection do |conn|
282
+ conn.respond_to?(:supports_setting_primary_key_of_imported_objects?) && conn.supports_setting_primary_key_of_imported_objects?
283
+ end
284
+ end
284
285
  end
285
286
 
286
287
  # Imports a collection of values to the database.
@@ -348,7 +349,7 @@ class ActiveRecord::Base
348
349
  # (if false) even if record timestamps is disabled in ActiveRecord::Base
349
350
  # * +recursive+ - true|false, tells import to import all has_many/has_one
350
351
  # associations if the adapter supports setting the primary keys of the
351
- # newly imported objects. PostgreSQL only.
352
+ # newly imported objects. PostgreSQL and MariaDB 10.5+ only.
352
353
  # * +batch_size+ - an integer value to specify the max number of records to
353
354
  # include per insert. Defaults to the total number of records to import.
354
355
  #
@@ -403,7 +404,8 @@ class ActiveRecord::Base
403
404
  # ==== Using :all
404
405
  #
405
406
  # 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
+ # other than the primary key are updated, except for timestamps recorded
408
+ # on create, such as created_at. If a list of column names is
407
409
  # supplied, only those columns will be updated. Below is an example:
408
410
  #
409
411
  # BlogPost.import columns, values, on_duplicate_key_update: :all
@@ -434,7 +436,8 @@ class ActiveRecord::Base
434
436
  # ==== Using :all
435
437
  #
436
438
  # 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
439
+ # other than the primary key are updated, except for timestamps recorded
440
+ # on create, such as created_at. If a list of column names is
438
441
  # supplied, only those columns will be updated. Below is an example:
439
442
  #
440
443
  # BlogPost.import columns, values, on_duplicate_key_update: :all
@@ -499,7 +502,8 @@ class ActiveRecord::Base
499
502
  #
500
503
  # ===== Using :all
501
504
  #
502
- # The :columns attribute can be :all. All columns other than the primary key will be updated.
505
+ # The :columns attribute can be :all. All columns other than the primary key will be updated,
506
+ # except for timestamps recorded on create, such as created_at.
503
507
  # If a list of column names is supplied, only those columns will be updated.
504
508
  # Below is an example:
505
509
  #
@@ -538,219 +542,101 @@ class ActiveRecord::Base
538
542
  import_helper(*args)
539
543
  end
540
544
  end
541
- alias import bulk_import unless ActiveRecord::Base.respond_to? :import
542
545
 
543
546
  # Imports a collection of values if all values are valid. Import fails at the
544
547
  # first encountered validation error and raises ActiveRecord::RecordInvalid
545
548
  # with the failed instance.
546
549
  def bulk_import!(*args)
547
550
  options = args.last.is_a?( Hash ) ? args.pop : {}
548
- options[:validate] = true
549
- options[:raise_error] = true
551
+ options = options.merge( validate: true, raise_error: true )
550
552
 
551
553
  bulk_import(*args, options)
552
554
  end
553
- alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
554
-
555
- def import_helper( *args )
556
- options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
557
- options.merge!( args.pop ) if args.last.is_a? Hash
558
- # making sure that current model's primary key is used
559
- options[:primary_key] = primary_key
560
- options[:locking_column] = locking_column if locking_enabled?
561
-
562
- is_validating = options[:validate_with_context].present? || options[:validate]
563
- validator = ActiveRecord::Import::Validator.new(self, options)
564
-
565
- # assume array of model objects
566
- if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
567
- if args.length == 2
568
- models = args.last
569
- column_names = args.first.dup
570
- else
571
- models = args.first
572
- column_names = if connection.respond_to?(:supports_virtual_columns?) && connection.supports_virtual_columns?
573
- columns.reject(&:virtual?).map(&:name)
574
- else
575
- self.column_names.dup
576
- end
577
- end
578
555
 
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
585
- end
556
+ # Returns the column for +name+, following an attribute alias unless +name+ is
557
+ # itself a column, so a column that's also an alias isn't replaced by the
558
+ # column it aliases.
559
+ def column_name_for_import(name) # :nodoc:
560
+ name = name.to_s
561
+ attribute_alias?(name) && !columns_hash.key?(name) ? attribute_alias(name) : name
562
+ end
586
563
 
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
593
- end
564
+ def import_helper( *args )
565
+ connection_pool.with_connection do |conn|
566
+ # The adapter's import support is loaded when the connection is established, which is
567
+ # missed if activerecord-import is required afterwards, so load it now if it's missing.
568
+ ActiveRecord::Import.load_from_connection_pool_once(connection_pool) unless supports_import?(conn)
594
569
 
595
- array_of_attributes = []
570
+ options, args = options_and_arguments_for_import(args)
596
571
 
597
- models.each do |model|
598
- if supports_setting_primary_key_of_imported_objects?
599
- load_association_ids(model)
600
- end
572
+ is_validating = options[:validate_with_context].present? || options[:validate]
573
+ validator = ActiveRecord::Import::Validator.new(options) if is_validating
601
574
 
602
- if is_validating && !validator.valid_model?(model)
603
- raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
604
- next
605
- end
575
+ # supports empty array
576
+ return ActiveRecord::Import::Result.new([], 0, [], []) if args.last.is_a?( Array ) && args.last.empty?
606
577
 
607
- array_of_attributes << column_names.map do |name|
608
- if model.persisted? &&
609
- update_attrs && update_attrs.include?(name.to_sym) &&
610
- !model.send("#{name}_changed?")
611
- nil
612
- else
613
- model.read_attribute(name.to_s)
614
- end
615
- end
616
- end
617
- # supports array of hash objects
618
- elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
619
- if args.length == 2
620
- array_of_hashes = args.last
621
- column_names = args.first.dup
622
- allow_extra_hash_keys = true
578
+ # assume array of model objects
579
+ if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
580
+ models, column_names, default_column_names = models_and_column_names_for_import(conn, args)
581
+ array_of_attributes = attributes_from_models(conn, models, column_names, validator, options)
623
582
  else
624
- array_of_hashes = args.first
625
- column_names = array_of_hashes.first.keys
626
- allow_extra_hash_keys = false
583
+ column_names, array_of_attributes = column_names_and_attributes_for_import(args)
627
584
  end
628
585
 
629
- array_of_attributes = array_of_hashes.map do |h|
630
- error_message = validate_hash_import(h, column_names, allow_extra_hash_keys)
631
-
632
- raise ArgumentError, error_message if error_message
633
-
634
- column_names.map do |key|
635
- h[key]
636
- end
637
- end
638
- # supports empty array
639
- elsif args.last.is_a?( Array ) && args.last.empty?
640
- return ActiveRecord::Import::Result.new([], 0, [], [])
641
- # supports 2-element array and array
642
- elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
586
+ # Rows without a value for these columns get the database's default rather than NULL
587
+ options[:database_default_columns] = Array(default_column_names) | database_populated_primary_keys(conn)
643
588
 
644
- unless args.last.first.is_a?(Array)
645
- raise ArgumentError, "Last argument should be a two dimensional array '[[]]'. First element in array was a #{args.last.first.class}"
589
+ # Don't modify incoming arguments
590
+ on_duplicate_key_update = options[:on_duplicate_key_update]
591
+ if on_duplicate_key_update
592
+ options[:on_duplicate_key_update] = on_duplicate_key_update_for_import(on_duplicate_key_update, column_names, options)
646
593
  end
647
594
 
648
- column_names, array_of_attributes = args
595
+ add_primary_key_for_sequence(conn, column_names, array_of_attributes)
649
596
 
650
- # dup the passed args so we don't modify unintentionally
651
- column_names = column_names.dup
652
- array_of_attributes = array_of_attributes.map(&:dup)
653
- else
654
- raise ArgumentError, "Invalid arguments!"
655
- end
656
-
657
- # Force the primary key col into the insert if it's not
658
- # on the list and we are using a sequence and stuff a nil
659
- # value for it into each row so the sequencer will fire later
660
- symbolized_column_names = Array(column_names).map(&:to_sym)
661
- symbolized_primary_key = Array(primary_key).map(&:to_sym)
597
+ timestamps = {}
662
598
 
663
- if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && connection.prefetch_primary_key? && sequence_name
664
- column_count = column_names.size
665
- column_names.concat(Array(primary_key)).uniq!
666
- columns_added = column_names.size - column_count
667
- new_fields = Array.new(columns_added)
668
- array_of_attributes.each { |a| a.concat(new_fields) }
669
- end
599
+ # record timestamps unless disabled in ActiveRecord::Base
600
+ if record_timestamps && options[:timestamps]
601
+ timestamps = add_special_rails_stamps conn, column_names, array_of_attributes, options
602
+ end
670
603
 
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
604
+ return_obj = if is_validating
605
+ import_with_validations( conn, column_names, array_of_attributes, options ) do |failed_instances|
606
+ if models
607
+ add_failed_models(failed_instances, models, options)
681
608
  else
682
- v
609
+ validate_attributes_for_import(failed_instances, column_names, array_of_attributes, validator, options)
683
610
  end
684
611
  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
612
  else
690
- on_duplicate_key_update
613
+ import_without_validations_or_callbacks( conn, column_names, array_of_attributes, options )
691
614
  end
692
- end
693
615
 
694
- timestamps = {}
616
+ return return_obj if options[:all_or_none] && return_obj.failed_instances.any?
695
617
 
696
- # record timestamps unless disabled in ActiveRecord::Base
697
- if record_timestamps && options[:timestamps]
698
- timestamps = add_special_rails_stamps column_names, array_of_attributes, options
699
- end
618
+ if options[:synchronize]
619
+ sync_keys = options[:synchronize_keys] || Array(primary_key)
620
+ synchronized_instances = synchronize_imported_instances(options[:synchronize], sync_keys)
621
+ end
622
+ return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
700
623
 
701
- return_obj = if is_validating
702
- import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
703
- if models
704
- models.each_with_index do |m, i|
705
- next unless m.errors.any?
624
+ # if we have ids, then set the id on the models and mark the models as clean.
625
+ if models && supports_setting_primary_key_of_imported_objects?(conn)
626
+ set_attributes_and_mark_clean(conn, models, return_obj, timestamps, options)
706
627
 
707
- failed_instances << (options[:track_validation_failures] ? [i, m] : m)
708
- end
709
- else
710
- # create instances for each of our column/value sets
711
- arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
712
-
713
- # keep track of the instance and the position it is currently at. if this fails
714
- # validation we'll use the index to remove it from the array_of_attributes
715
- arr.each_with_index do |hsh, i|
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
-
721
- next if validator.valid_model?(model)
722
- raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
723
-
724
- array_of_attributes[i] = nil
725
- failure = model.dup
726
- failure.errors.send(:initialize_dup, model.errors)
727
- failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
728
- end
729
- array_of_attributes.compact!
628
+ # if there are auto-save associations on the models we imported that are new, import them as well
629
+ if options[:recursive]
630
+ import_associations_of_imported_models(models, return_obj, on_duplicate_key_update, options)
730
631
  end
731
632
  end
732
- else
733
- import_without_validations_or_callbacks( column_names, array_of_attributes, options )
734
- end
735
-
736
- if options[:synchronize]
737
- sync_keys = options[:synchronize_keys] || Array(primary_key)
738
- synchronize( options[:synchronize], sync_keys)
739
- end
740
- return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
741
633
 
742
- # if we have ids, then set the id on the models and mark the models as clean.
743
- if models && supports_setting_primary_key_of_imported_objects?
744
- set_attributes_and_mark_clean(models, return_obj, timestamps, options)
634
+ # apply the changes of synchronized instances that weren't marked clean above,
635
+ # such as when importing columns and values rather than models
636
+ synchronized_instances&.each { |instance| instance.changes_applied if instance.changed? }
745
637
 
746
- # if there are auto-save associations on the models we imported that are new, import them as well
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
638
+ return_obj
751
639
  end
752
-
753
- return_obj
754
640
  end
755
641
 
756
642
  # Imports the passed in +column_names+ and +array_of_attributes+
@@ -760,7 +646,7 @@ class ActiveRecord::Base
760
646
  # +num_inserts+ is the number of inserts it took to import the data. See
761
647
  # ActiveRecord::Base.import for more information on
762
648
  # +column_names+, +array_of_attributes+ and +options+.
763
- def import_with_validations( column_names, array_of_attributes, options = {} )
649
+ def import_with_validations( conn, column_names, array_of_attributes, options = {} )
764
650
  failed_instances = []
765
651
 
766
652
  yield failed_instances if block_given?
@@ -768,9 +654,9 @@ class ActiveRecord::Base
768
654
  result = if options[:all_or_none] && failed_instances.any?
769
655
  ActiveRecord::Import::Result.new([], 0, [], [])
770
656
  else
771
- import_without_validations_or_callbacks( column_names, array_of_attributes, options )
657
+ import_without_validations_or_callbacks( conn, column_names, array_of_attributes, options )
772
658
  end
773
- ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results)
659
+ ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results, result.affected_rows)
774
660
  end
775
661
 
776
662
  # Imports the passed in +column_names+ and +array_of_attributes+
@@ -779,12 +665,11 @@ class ActiveRecord::Base
779
665
  # validations or callbacks. See ActiveRecord::Base.import for more
780
666
  # information on +column_names+, +array_of_attributes_ and
781
667
  # +options+.
782
- def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
668
+ def import_without_validations_or_callbacks( conn, column_names, array_of_attributes, options = {} )
783
669
  return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
784
670
 
785
671
  column_names = column_names.map do |name|
786
- original_name = attribute_alias?(name) ? attribute_alias(name) : name
787
- original_name.to_sym
672
+ column_name_for_import(name).to_sym
788
673
  end
789
674
  scope_columns, scope_values = scope_attributes.to_a.transpose
790
675
 
@@ -803,23 +688,27 @@ class ActiveRecord::Base
803
688
  end
804
689
 
805
690
  columns = column_names.each_with_index.map do |name, i|
806
- column = schema_columns_hash[name.to_s]
691
+ column = schema_columns_hash(conn)[name.to_s]
807
692
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
808
693
  column
809
694
  end
810
695
 
811
- columns_sql = "(#{column_names.map { |name| connection.quote_column_name(name) }.join(',')})"
812
- pre_sql_statements = connection.pre_sql_statements( options )
696
+ columns_sql = "(#{column_names.map { |name| conn.quote_column_name(name) }.join(',')})"
697
+ pre_sql_statements = conn.pre_sql_statements( options )
813
698
  insert_sql = ['INSERT', pre_sql_statements, "INTO #{quoted_table_name} #{columns_sql} VALUES "]
814
699
  insert_sql = insert_sql.flatten.join(' ')
815
- values_sql = values_sql_for_columns_and_attributes(columns, array_of_attributes)
700
+ values_sql = values_sql_for_columns_and_attributes(conn, columns, array_of_attributes, Array(options[:database_default_columns]))
701
+ comment_sql = sql_comment_for_annotations(conn)
816
702
 
817
703
  number_inserted = 0
818
704
  ids = []
819
705
  results = []
820
- if supports_import?
706
+ affected_rows = 0
707
+
708
+ if supports_import?(conn)
821
709
  # generate the sql
822
- post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
710
+ post_sql_statements = conn.post_sql_statements( quoted_table_name, options )
711
+ post_sql_statements += [comment_sql] if comment_sql
823
712
  import_size = values_sql.size
824
713
 
825
714
  batch_size = options[:batch_size] || import_size
@@ -832,31 +721,269 @@ class ActiveRecord::Base
832
721
  batch_started_at = Time.now.to_i
833
722
 
834
723
  # perform the inserts
835
- result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
724
+ result = conn.insert_many( [insert_sql, post_sql_statements].flatten,
836
725
  batch_values,
837
726
  options,
838
727
  "#{model_name} Create Many" )
839
728
 
840
729
  number_inserted += result.num_inserts
841
- ids += result.ids
842
- results += result.results
730
+ ids.concat(result.ids)
731
+ results.concat(result.results)
732
+ affected_rows = result.affected_rows.nil? ? nil : (affected_rows || 0) + result.affected_rows
843
733
  current_batch += 1
844
734
 
845
735
  progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
846
736
  end
847
737
  else
738
+ logger&.warn "activerecord-import: the #{conn.adapter_name} adapter doesn't support bulk import, so #{model_name} " \
739
+ "records are being inserted one at a time. If the adapter is supported under another name, require its " \
740
+ "activerecord-import adapter, e.g. activerecord-import/active_record/adapters/postgresql_adapter."
741
+
848
742
  transaction(requires_new: true) do
849
743
  values_sql.each do |values|
850
- ids << connection.insert(insert_sql + values)
744
+ ids << conn.insert("#{insert_sql}#{values}#{comment_sql}", "#{model_name} Create Many")
851
745
  number_inserted += 1
852
746
  end
853
747
  end
854
748
  end
855
- ActiveRecord::Import::Result.new([], number_inserted, ids, results)
749
+ ActiveRecord::Import::Result.new([], number_inserted, ids, results, affected_rows)
856
750
  end
857
751
 
858
752
  private
859
753
 
754
+ # Takes the options Hash, if any, off the end of +args+ and returns the options
755
+ # for the import, with their defaults, along with the arguments to import.
756
+ def options_and_arguments_for_import(args)
757
+ options = { validate: true, timestamps: true, track_validation_failures: false }
758
+ options.merge!( args.pop ) if args.last.is_a? Hash
759
+ args = arrays_from_array_like_import_arguments(args)
760
+ # making sure that the current model and its primary key are used, rather than
761
+ # the parent model's when importing associations recursively
762
+ options[:model] = self
763
+ options[:primary_key] = primary_key
764
+ options[:locking_column] = locking_column if locking_enabled?
765
+ [options, args]
766
+ end
767
+
768
+ # Returns the models to import from +args+, the column names to import them with,
769
+ # and the columns the database populates for the models that don't assign them,
770
+ # which is nil when +args+ give the column names.
771
+ def models_and_column_names_for_import(conn, args)
772
+ if args.length == 2
773
+ models = args.last
774
+ column_names = args.first.dup
775
+ else
776
+ models = args.first
777
+ column_names = if conn.try(:supports_virtual_columns?)
778
+ columns.reject(&:virtual?).map(&:name)
779
+ else
780
+ self.column_names.dup
781
+ end
782
+ populated_column_names = database_populated_column_names(conn, column_names, models)
783
+ # Leave out the ones no model assigns, and let the database fill in the rest for the models that don't
784
+ default_column_names = populated_column_names.select { |name| models.any? { |model| model.attribute_changed?(name) } }
785
+ column_names -= populated_column_names - default_column_names
786
+ end
787
+
788
+ # Leave out a primary key the database populates, like a uuid, only when no model has one, so
789
+ # assigned ids are kept. Otherwise the database fills it in for the models without one.
790
+ column_names -= database_populated_primary_keys(conn) if models.all? { |model| model.id.nil? }
791
+
792
+ [models, column_names, default_column_names]
793
+ end
794
+
795
+ # Returns the values of +column_names+ for each of +models+, the rows to import. Models
796
+ # that +validator+ finds invalid are left out, or raise ActiveRecord::RecordInvalid with
797
+ # the :raise_error option. Without a +validator+, the models aren't validated.
798
+ def attributes_from_models(conn, models, column_names, validator, options)
799
+ update_attrs = if record_timestamps && options[:timestamps]
800
+ send(:timestamp_attributes_for_update).map(&:to_sym)
801
+ end
802
+
803
+ array_of_attributes = []
804
+
805
+ models.each do |model|
806
+ if supports_setting_primary_key_of_imported_objects?(conn)
807
+ load_association_ids(model)
808
+ end
809
+
810
+ if validator && !validator.valid_model?(model)
811
+ raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
812
+ next
813
+ end
814
+
815
+ array_of_attributes << column_names.map do |name|
816
+ if model.persisted? &&
817
+ update_attrs && update_attrs.include?(name.to_sym) &&
818
+ !model.send("#{name}_changed?")
819
+ nil
820
+ else
821
+ name = name.to_s
822
+ # Read a column's own value, as saving the model would, even if it's also an alias
823
+ value = columns_hash.key?(name) ? model._read_attribute(name) : model.read_attribute(name)
824
+ model.instance_of?(self) ? value : value_from_model_type(model, name, value)
825
+ end
826
+ end
827
+ end
828
+
829
+ array_of_attributes
830
+ end
831
+
832
+ # Returns the column names and the rows to import from +args+, which are either an
833
+ # array of hashes, optionally after column names, or column names and an array of values.
834
+ def column_names_and_attributes_for_import(args)
835
+ # supports array of hash objects
836
+ if args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
837
+ if args.length == 2
838
+ array_of_hashes = args.last
839
+ column_names = args.first.dup
840
+ allow_extra_hash_keys = true
841
+ else
842
+ array_of_hashes = args.first
843
+ column_names = array_of_hashes.first.keys
844
+ allow_extra_hash_keys = false
845
+ end
846
+
847
+ array_of_attributes = array_of_hashes.map do |h|
848
+ error_message = validate_hash_import(h, column_names, allow_extra_hash_keys)
849
+
850
+ raise ArgumentError, error_message if error_message
851
+
852
+ column_names.map do |key|
853
+ h[key]
854
+ end
855
+ end
856
+ # supports 2-element array and array
857
+ elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
858
+
859
+ unless args.last.first.is_a?(Array)
860
+ raise ArgumentError, "Last argument should be a two dimensional array '[[]]'. First element in array was a #{args.last.first.class}"
861
+ end
862
+
863
+ column_names, array_of_attributes = args
864
+
865
+ # dup the passed args so we don't modify unintentionally
866
+ column_names = column_names.dup
867
+ array_of_attributes = array_of_attributes.map(&:dup)
868
+ else
869
+ raise ArgumentError, invalid_import_arguments_message(args)
870
+ end
871
+
872
+ [column_names, array_of_attributes]
873
+ end
874
+
875
+ # Returns a copy of the +on_duplicate_key_update+ option to import with, so it can be
876
+ # changed, such as when adding timestamp columns, without modifying the option that was
877
+ # passed in. :all is replaced by the columns other than the primary key and the
878
+ # timestamps recorded on create.
879
+ def on_duplicate_key_update_for_import(on_duplicate_key_update, column_names, options)
880
+ symbolized_column_names = Array(column_names).map(&:to_sym)
881
+ symbolized_primary_key = Array(primary_key).map(&:to_sym)
882
+
883
+ updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
884
+ # Like the primary key, timestamps recorded on create shouldn't change when a record is updated
885
+ if record_timestamps && options[:timestamps]
886
+ updatable_columns -= timestamp_attributes_for_create_in_model.map(&:to_sym)
887
+ end
888
+
889
+ if on_duplicate_key_update.is_a?(Hash)
890
+ on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
891
+ duped_options[k] = if k == :columns && v == :all
892
+ updatable_columns
893
+ elsif v.duplicable?
894
+ v.dup
895
+ else
896
+ v
897
+ end
898
+ end
899
+ elsif on_duplicate_key_update == :all
900
+ updatable_columns
901
+ elsif on_duplicate_key_update.duplicable?
902
+ on_duplicate_key_update.dup
903
+ else
904
+ on_duplicate_key_update
905
+ end
906
+ end
907
+
908
+ # Force the primary key col into the insert if it's not
909
+ # on the list and we are using a sequence and stuff a nil
910
+ # value for it into each row so the sequencer will fire later
911
+ def add_primary_key_for_sequence(conn, column_names, array_of_attributes)
912
+ symbolized_column_names = Array(column_names).map(&:to_sym)
913
+ symbolized_primary_key = Array(primary_key).map(&:to_sym)
914
+
915
+ if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && conn.prefetch_primary_key? && sequence_name
916
+ column_count = column_names.size
917
+ column_names.concat(Array(primary_key)).uniq!
918
+ columns_added = column_names.size - column_count
919
+ new_fields = Array.new(columns_added)
920
+ array_of_attributes.each { |a| a.concat(new_fields) }
921
+ end
922
+ end
923
+
924
+ # Adds the models that failed validation, when their rows were read, to +failed_instances+.
925
+ def add_failed_models(failed_instances, models, options)
926
+ models.each_with_index do |m, i|
927
+ next unless m.errors.any?
928
+
929
+ failed_instances << (options[:track_validation_failures] ? [i, m] : m)
930
+ end
931
+ end
932
+
933
+ # Validates each row of +array_of_attributes+ as a new model, adding the models that
934
+ # fail to +failed_instances+ and removing their rows from +array_of_attributes+.
935
+ def validate_attributes_for_import(failed_instances, column_names, array_of_attributes, validator, options)
936
+ # create instances for each of our column/value sets
937
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
938
+
939
+ # keep track of the instance and the position it is currently at. if this fails
940
+ # validation we'll use the index to remove it from the array_of_attributes
941
+ arr.each_with_index do |hsh, i|
942
+ # utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
943
+ model = new do |m|
944
+ hsh.each_pair do |k, v|
945
+ if write_id_column_directly?(k)
946
+ m._write_attribute("id", v)
947
+ else
948
+ m[k] = v
949
+ end
950
+ end
951
+ end
952
+
953
+ next if validator.valid_model?(model)
954
+ raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
955
+
956
+ array_of_attributes[i] = nil
957
+ failure = model.dup
958
+ failure.errors.send(:initialize_dup, model.errors)
959
+ failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
960
+ end
961
+ array_of_attributes.compact!
962
+ end
963
+
964
+ # Imports the new and changed associations of the +models+ that were imported, with the
965
+ # :on_duplicate_key_update option that was passed in rather than the copy used for +models+.
966
+ def import_associations_of_imported_models(models, import_result, on_duplicate_key_update, options)
967
+ options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
968
+ import_associations(imported_models(models, import_result, options), options.dup.merge(validate: false))
969
+ end
970
+
971
+ # ActiveRecord treats "id" as the primary key when writing attributes
972
+ # through id= or []=. When the primary key is composite or is another
973
+ # column, an "id" column has to be written directly instead.
974
+ def write_id_column_directly?(column)
975
+ column.to_s == "id" && primary_key != "id"
976
+ end
977
+
978
+ # Returns SQL comments for the annotations on the current scope, as
979
+ # ActiveRecord adds them to its queries, or nil if there are none.
980
+ def sql_comment_for_annotations(conn)
981
+ annotations = all.annotate_values
982
+ return if annotations.empty?
983
+
984
+ " #{annotations.map { |value| "/* #{conn.sanitize_as_sql_comment(value)} */" }.join(' ')}"
985
+ end
986
+
860
987
  def associated_options(options, association)
861
988
  return options unless options.key?(:recursive_on_duplicate_key_update)
862
989
 
@@ -865,9 +992,55 @@ class ActiveRecord::Base
865
992
  )
866
993
  end
867
994
 
868
- def set_attributes_and_mark_clean(models, import_result, timestamps, options)
995
+ # Returns the models that weren't rejected by validations.
996
+ def imported_models(models, import_result, options)
997
+ failed_instances = import_result.failed_instances
998
+ failed_instances = failed_instances.map(&:last) if options[:track_validation_failures]
999
+ models - failed_instances
1000
+ end
1001
+
1002
+ # Synchronizes +instances+ with the database like ActiveRecord::Base.synchronize,
1003
+ # but rather than discarding their changes, it assigns the database values
1004
+ # on top of them. Once the instances are marked clean, previous_changes then
1005
+ # reflects what the import changed, as it does without synchronizing.
1006
+ # Instances left without changes are marked clean right away. Returns the
1007
+ # instances that were synchronized.
1008
+ def synchronize_imported_instances(instances, keys)
1009
+ synchronize_instances(instances, keys) do |instance, fresh_attributes|
1010
+ attributes = instance.instance_variable_get(:@attributes)
1011
+ # the attributes of destroyed records are frozen
1012
+ attributes = attributes.deep_dup if attributes.frozen?
1013
+
1014
+ names = fresh_attributes.keys
1015
+ if names.all? { |name| attributes.key?(name) }
1016
+ names.each do |name|
1017
+ value = fresh_attributes.fetch_value(name)
1018
+ attributes.write_from_user(name, value) unless same_database_value?(attributes.fetch_value(name), value)
1019
+ end
1020
+ instance.instance_variable_set :@attributes, attributes
1021
+ instance.changes_applied unless instance.changed?
1022
+ else
1023
+ # the instance doesn't have all of the attributes, such as when it was
1024
+ # loaded with select, so take the record's attributes as they are
1025
+ instance.instance_variable_set :@attributes, fresh_attributes
1026
+ instance.clear_changes_information
1027
+ end
1028
+ end
1029
+ end
1030
+
1031
+ # Returns true if +database_value+, reloaded from the database, is the +value+ an instance
1032
+ # already has. Databases store times to the microsecond, so a time with more precision,
1033
+ # which ActiveRecord < 7.0 keeps in memory, comes back rounded or truncated.
1034
+ def same_database_value?(value, database_value)
1035
+ return true if value == database_value
1036
+ return false unless value.is_a?(Time) && database_value.is_a?(Time)
1037
+
1038
+ value.round(6) == database_value || value.floor(6) == database_value
1039
+ end
1040
+
1041
+ def set_attributes_and_mark_clean(conn, models, import_result, timestamps, options)
869
1042
  return if models.nil?
870
- models -= import_result.failed_instances
1043
+ models = imported_models(models, import_result, options)
871
1044
 
872
1045
  # if ids were returned for all models we know all were updated
873
1046
  if models.size == import_result.ids.size
@@ -882,22 +1055,20 @@ class ActiveRecord::Base
882
1055
  end
883
1056
 
884
1057
  deserialize_value = lambda do |column, value|
885
- column = schema_columns_hash[column]
1058
+ column = schema_columns_hash(conn)[column]
886
1059
  return value unless column
887
- if respond_to?(:type_caster)
888
- type = type_for_attribute(column.name)
889
- type.deserialize(value)
890
- elsif column.respond_to?(:type_cast_from_database)
891
- column.type_cast_from_database(value)
892
- else
893
- value
894
- end
1060
+ type = type_for_attribute(column.name)
1061
+ type.deserialize(value)
895
1062
  end
896
1063
 
897
1064
  set_value = lambda do |model, column, value|
898
1065
  val = deserialize_value.call(column, value)
899
1066
  if model.attribute_names.include?(column)
900
- model.send("#{column}=", val)
1067
+ if write_id_column_directly?(column)
1068
+ model._write_attribute(column, val)
1069
+ else
1070
+ model.send("#{column}=", val)
1071
+ end
901
1072
  else
902
1073
  attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
903
1074
  model.instance_variable_set(:@attributes, attributes)
@@ -922,14 +1093,7 @@ class ActiveRecord::Base
922
1093
  end
923
1094
 
924
1095
  models.each do |model|
925
- if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
926
- model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
927
- model.changes_applied
928
- elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
929
- model.clear_changes_information
930
- else # Rails 3.2
931
- model.instance_variable_get(:@changed_attributes).clear
932
- end
1096
+ model.changes_applied
933
1097
  model.instance_variable_set(:@new_record, false)
934
1098
  end
935
1099
  end
@@ -949,12 +1113,24 @@ class ActiveRecord::Base
949
1113
  association = association.target
950
1114
  next if association.blank? || model.public_send(column_name).present?
951
1115
 
952
- association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
1116
+ association_primary_key = association_primary_key_names(association_reflection)[column_index]
1117
+ next unless association_primary_key
1118
+
953
1119
  model.public_send("#{column_name}=", association.send(association_primary_key))
954
1120
  end
955
1121
  end
956
1122
  end
957
1123
 
1124
+ # Returns the names of the columns that a belongs_to association's foreign key refers to.
1125
+ # For a composite key, ActiveRecord 7.1+ returns an Array, while older versions return
1126
+ # its string form, e.g. "[:account_id, :id]".
1127
+ def association_primary_key_names(association_reflection)
1128
+ association_primary_key = association_reflection.association_primary_key
1129
+ return association_primary_key.map(&:to_s) if association_primary_key.is_a?(Array)
1130
+
1131
+ association_primary_key.tr("[]:", "").split(", ")
1132
+ end
1133
+
958
1134
  def import_associations(models, options)
959
1135
  # now, for all the dirty associations, collect them into a new set of models, then recurse.
960
1136
  # notes:
@@ -980,9 +1156,46 @@ class ActiveRecord::Base
980
1156
  end
981
1157
  end
982
1158
 
983
- def schema_columns_hash
984
- if respond_to?(:ignored_columns) && ignored_columns.any?
985
- connection.schema_cache.columns_hash(table_name)
1159
+ # Returns the columns the database populates itself, such as with a default
1160
+ # function, when importing new +models+. Like ActiveRecord's own inserts, the
1161
+ # database populates these for models that don't assign a value, instead of
1162
+ # inserting NULL. The primary key is handled separately.
1163
+ def database_populated_column_names(conn, column_names, models)
1164
+ return [] if models.any?(&:persisted?)
1165
+
1166
+ columns_hash = schema_columns_hash(conn)
1167
+ primary_keys = Array(primary_key).map(&:to_s)
1168
+
1169
+ column_names.select do |name|
1170
+ column = columns_hash[name.to_s]
1171
+ next false if column.nil? || primary_keys.include?(name.to_s)
1172
+
1173
+ column.respond_to?(:auto_populated?) ? column.auto_populated? : column.default_function
1174
+ end
1175
+ end
1176
+
1177
+ # Returns the primary key columns the database populates itself, such as a uuid
1178
+ # or a key with a default function, rather than from the model's sequence.
1179
+ def database_populated_primary_keys(conn)
1180
+ Array(primary_key).select do |name|
1181
+ column = schema_columns_hash(conn)[name]
1182
+ column && (column.type == :uuid || (column.default_function && sequence_name.blank?))
1183
+ end
1184
+ end
1185
+
1186
+ # A model of another class, such as an STI subclass with its own enum, can give
1187
+ # a column a different type than this class, which serializes the imported values.
1188
+ # Convert the model's value to this class's type through its database value.
1189
+ def value_from_model_type(model, name, value)
1190
+ name = column_name_for_import(name)
1191
+ model_type = model.class.type_for_attribute(name)
1192
+ type = type_for_attribute(name)
1193
+ model_type.equal?(type) ? value : type.deserialize(model_type.serialize(value))
1194
+ end
1195
+
1196
+ def schema_columns_hash(conn)
1197
+ if ignored_columns.any?
1198
+ conn.schema_cache.columns_hash(table_name)
986
1199
  else
987
1200
  columns_hash
988
1201
  end
@@ -1006,18 +1219,25 @@ class ActiveRecord::Base
1006
1219
  # Wrap target in an array if not already
1007
1220
  association = Array(association.target)
1008
1221
 
1009
- changed_objects = association.select { |a| a.new_record? || a.changed? }
1222
+ foreign_key_columns = Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key)
1223
+ foreign_key_values = association_foreign_key_values(model, association_reflection)
1224
+
1225
+ # For polymorphic associations
1226
+ association_name = model.class.polymorphic_name
1227
+
1228
+ # Import new and changed records, and existing records that were assigned to
1229
+ # this model but aren't linked to it yet, as saving the model would
1230
+ changed_objects = association.select do |child|
1231
+ child.new_record? || child.changed? ||
1232
+ (!association_reflection.through_reflection? &&
1233
+ reassigned_to_parent?(child, foreign_key_columns, foreign_key_values, association_reflection.type, association_name))
1234
+ end
1235
+
1010
1236
  changed_objects.each do |child|
1011
- Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
1012
- child.public_send("#{column}=", Array(model.id)[index])
1237
+ foreign_key_columns.each_with_index do |column, index|
1238
+ child.public_send("#{column}=", foreign_key_values[index])
1013
1239
  end
1014
1240
 
1015
- # For polymorphic associations
1016
- association_name = if model.class.respond_to?(:polymorphic_name)
1017
- model.class.polymorphic_name
1018
- else
1019
- model.class.base_class
1020
- end
1021
1241
  association_reflection.type.try do |type|
1022
1242
  child.public_send("#{type}=", association_name)
1023
1243
  end
@@ -1027,37 +1247,55 @@ class ActiveRecord::Base
1027
1247
  associated_objects_by_class
1028
1248
  end
1029
1249
 
1030
- # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
1031
- # and +array_of_attributes+.
1032
- def values_sql_for_columns_and_attributes(columns, array_of_attributes) # :nodoc:
1033
- # connection gets called a *lot* in this high intensity loop.
1034
- # Reuse the same one w/in the loop, otherwise it would keep being re-retreived (= lots of time for large imports)
1035
- connection_memo = connection
1250
+ # Returns the values of +model+ that the foreign key of +association_reflection+
1251
+ # refers to: the columns named by the association's :primary_key option when
1252
+ # one is given (e.g. <tt>has_many :reviews, primary_key: :identifier</tt>),
1253
+ # and the model's id otherwise.
1254
+ def association_foreign_key_values(model, association_reflection)
1255
+ if association_reflection.options[:primary_key]
1256
+ Array(association_reflection.active_record_primary_key).map { |key| model.read_attribute(key) }
1257
+ else
1258
+ Array(model.id)
1259
+ end
1260
+ end
1261
+
1262
+ # Returns true if +child+, an existing record of a has_one or has_many association,
1263
+ # doesn't refer to the parent yet, such as when it was assigned to a new parent.
1264
+ def reassigned_to_parent?(child, foreign_key_columns, foreign_key_values, type_column, type_name)
1265
+ return false if child.new_record?
1036
1266
 
1267
+ parent_values = foreign_key_columns.zip(foreign_key_values)
1268
+ parent_values << [type_column, type_name.to_s] if type_column.present?
1269
+ parent_values.any? { |column, value| child.has_attribute?(column) && child.read_attribute(column) != value }
1270
+ end
1271
+
1272
+ # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
1273
+ # and +array_of_attributes+. Nil values for +default_column_names+ use the
1274
+ # database's default.
1275
+ def values_sql_for_columns_and_attributes(conn, columns, array_of_attributes, default_column_names = []) # :nodoc:
1037
1276
  array_of_attributes.map do |arr|
1277
+ if arr.length < columns.length
1278
+ raise ArgumentError, "Number of values (#{arr.length}) is less than number of columns (#{columns.length})"
1279
+ end
1280
+
1038
1281
  my_values = arr.each_with_index.map do |val, j|
1039
1282
  column = columns[j]
1040
1283
 
1041
1284
  # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
1042
1285
  if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
1043
- connection_memo.next_value_for_sequence(sequence_name)
1286
+ conn.next_value_for_sequence(sequence_name)
1287
+ elsif val.nil? && default_column_names.include?(column.name)
1288
+ unless conn.supports_default_keyword_in_values?
1289
+ raise ArgumentError, "#{conn.adapter_name} doesn't support DEFAULT in VALUES, so the database can't populate " \
1290
+ "#{table_name}.#{column.name} for only the rows without a value. Import those rows separately so the column can be left out."
1291
+ end
1292
+ "DEFAULT"
1044
1293
  elsif val.respond_to?(:to_sql)
1045
1294
  "(#{val.to_sql})"
1046
1295
  elsif column
1047
- if respond_to?(:type_caster) # Rails 5.0 and higher
1048
- type = type_for_attribute(column.name)
1049
- val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
1050
- connection_memo.quote(val)
1051
- elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
1052
- connection_memo.quote(column.type_cast_from_user(val), column)
1053
- else # Rails 3.2, 4.0 and 4.1
1054
- if serialized_attributes.include?(column.name)
1055
- val = serialized_attributes[column.name].dump(val)
1056
- end
1057
- # Fixes #443 to support binary (i.e. bytea) columns on PG
1058
- val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
1059
- connection_memo.quote(val, column)
1060
- end
1296
+ type = type_for_attribute(column.name)
1297
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
1298
+ conn.quote(val)
1061
1299
  else
1062
1300
  raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
1063
1301
  end
@@ -1066,18 +1304,12 @@ class ActiveRecord::Base
1066
1304
  end
1067
1305
  end
1068
1306
 
1069
- def add_special_rails_stamps( column_names, array_of_attributes, options )
1307
+ def add_special_rails_stamps( conn, column_names, array_of_attributes, options )
1070
1308
  timestamp_columns = {}
1071
1309
  timestamps = {}
1072
1310
 
1073
- if respond_to?(:all_timestamp_attributes_in_model, true) # Rails 5.1 and higher
1074
- timestamp_columns[:create] = timestamp_attributes_for_create_in_model
1075
- timestamp_columns[:update] = timestamp_attributes_for_update_in_model
1076
- else
1077
- instance = allocate
1078
- timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
1079
- timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
1080
- end
1311
+ timestamp_columns[:create] = timestamp_attributes_for_create_in_model
1312
+ timestamp_columns[:update] = timestamp_attributes_for_update_in_model
1081
1313
 
1082
1314
  # use tz as set in ActiveRecord::Base
1083
1315
  default_timezone = if ActiveRecord.respond_to?(:default_timezone)
@@ -1101,8 +1333,8 @@ class ActiveRecord::Base
1101
1333
  array_of_attributes.each { |arr| arr << timestamp }
1102
1334
  end
1103
1335
 
1104
- if supports_on_duplicate_key_update? && action == :update
1105
- connection.add_column_for_on_duplicate_key_update(column, options)
1336
+ if supports_on_duplicate_key_update?(conn) && action == :update
1337
+ conn.add_column_for_on_duplicate_key_update(column, options)
1106
1338
  end
1107
1339
  end
1108
1340
  end
@@ -1110,6 +1342,17 @@ class ActiveRecord::Base
1110
1342
  timestamps
1111
1343
  end
1112
1344
 
1345
+ # Converts array-like arguments, such as a Relation or a Set, to arrays.
1346
+ def arrays_from_array_like_import_arguments(args) # :nodoc:
1347
+ args.map { |arg| arg.is_a?(Enumerable) && !arg.is_a?(Array) && !arg.is_a?(Hash) ? arg.to_a : arg }
1348
+ end
1349
+
1350
+ def invalid_import_arguments_message(args) # :nodoc:
1351
+ received = args.map { |arg| arg.is_a?(Array) && arg.any? ? "Array of #{arg.first.class}" : arg.class.name }
1352
+ "Invalid arguments! Expected an array of models, an array of hashes, or column names and an array " \
1353
+ "of values, but received #{received.empty? ? 'no arguments' : received.join(', ')}"
1354
+ end
1355
+
1113
1356
  # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
1114
1357
  def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
1115
1358
  array_of_attributes.map { |values| Hash[column_names.zip(values)] }