activerecord-import 0.14.1 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +448 -2
  7. data/Gemfile +26 -19
  8. data/LICENSE +21 -56
  9. data/README.markdown +568 -32
  10. data/Rakefile +5 -1
  11. data/activerecord-import.gemspec +8 -7
  12. data/benchmarks/README +2 -2
  13. data/benchmarks/benchmark.rb +8 -1
  14. data/benchmarks/lib/base.rb +2 -0
  15. data/benchmarks/lib/cli_parser.rb +5 -2
  16. data/benchmarks/lib/float.rb +2 -0
  17. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  18. data/benchmarks/lib/output_to_csv.rb +2 -0
  19. data/benchmarks/lib/output_to_html.rb +4 -2
  20. data/benchmarks/models/test_innodb.rb +2 -0
  21. data/benchmarks/models/test_memory.rb +2 -0
  22. data/benchmarks/models/test_myisam.rb +2 -0
  23. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  24. data/gemfiles/4.2.gemfile +4 -3
  25. data/gemfiles/5.0.gemfile +4 -3
  26. data/gemfiles/5.1.gemfile +4 -0
  27. data/gemfiles/5.2.gemfile +4 -0
  28. data/gemfiles/6.0.gemfile +4 -0
  29. data/gemfiles/6.1.gemfile +4 -0
  30. data/gemfiles/7.0.gemfile +4 -0
  31. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  33. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +8 -0
  35. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  38. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  39. data/lib/activerecord-import/adapters/abstract_adapter.rb +12 -16
  40. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  42. data/lib/activerecord-import/adapters/mysql_adapter.rb +35 -18
  43. data/lib/activerecord-import/adapters/postgresql_adapter.rb +107 -28
  44. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +141 -18
  45. data/lib/activerecord-import/base.rb +15 -8
  46. data/lib/activerecord-import/import.rb +642 -178
  47. data/lib/activerecord-import/mysql2.rb +2 -0
  48. data/lib/activerecord-import/postgresql.rb +2 -0
  49. data/lib/activerecord-import/sqlite3.rb +2 -0
  50. data/lib/activerecord-import/synchronize.rb +5 -3
  51. data/lib/activerecord-import/value_sets_parser.rb +29 -3
  52. data/lib/activerecord-import/version.rb +3 -1
  53. data/lib/activerecord-import.rb +5 -16
  54. data/test/adapters/jdbcmysql.rb +2 -0
  55. data/test/adapters/jdbcpostgresql.rb +2 -0
  56. data/test/adapters/jdbcsqlite3.rb +3 -0
  57. data/test/adapters/makara_postgis.rb +3 -0
  58. data/test/adapters/mysql2.rb +2 -0
  59. data/test/adapters/mysql2_makara.rb +2 -0
  60. data/test/adapters/mysql2spatial.rb +2 -0
  61. data/test/adapters/postgis.rb +2 -0
  62. data/test/adapters/postgresql.rb +2 -0
  63. data/test/adapters/postgresql_makara.rb +2 -0
  64. data/test/adapters/seamless_database_pool.rb +2 -0
  65. data/test/adapters/spatialite.rb +2 -0
  66. data/test/adapters/sqlite3.rb +2 -0
  67. data/test/{travis → github}/database.yml +7 -1
  68. data/test/import_test.rb +498 -32
  69. data/test/jdbcmysql/import_test.rb +2 -1
  70. data/test/jdbcpostgresql/import_test.rb +2 -1
  71. data/test/jdbcsqlite3/import_test.rb +6 -0
  72. data/test/makara_postgis/import_test.rb +10 -0
  73. data/test/models/account.rb +5 -0
  74. data/test/models/alarm.rb +4 -0
  75. data/test/models/animal.rb +8 -0
  76. data/test/models/bike_maker.rb +9 -0
  77. data/test/models/book.rb +4 -0
  78. data/test/models/car.rb +5 -0
  79. data/test/models/card.rb +5 -0
  80. data/test/models/chapter.rb +2 -0
  81. data/test/models/customer.rb +8 -0
  82. data/test/models/deck.rb +8 -0
  83. data/test/models/dictionary.rb +6 -0
  84. data/test/models/discount.rb +2 -0
  85. data/test/models/end_note.rb +2 -0
  86. data/test/models/group.rb +2 -0
  87. data/test/models/order.rb +8 -0
  88. data/test/models/playing_card.rb +4 -0
  89. data/test/models/promotion.rb +2 -0
  90. data/test/models/question.rb +2 -0
  91. data/test/models/rule.rb +2 -0
  92. data/test/models/tag.rb +7 -0
  93. data/test/models/tag_alias.rb +5 -0
  94. data/test/models/topic.rb +16 -0
  95. data/test/models/user.rb +5 -0
  96. data/test/models/user_token.rb +6 -0
  97. data/test/models/vendor.rb +9 -0
  98. data/test/models/widget.rb +18 -0
  99. data/test/mysql2/import_test.rb +2 -0
  100. data/test/mysql2_makara/import_test.rb +2 -0
  101. data/test/mysqlspatial2/import_test.rb +2 -0
  102. data/test/postgis/import_test.rb +6 -0
  103. data/test/postgresql/import_test.rb +2 -4
  104. data/test/schema/generic_schema.rb +88 -3
  105. data/test/schema/jdbcpostgresql_schema.rb +3 -0
  106. data/test/schema/mysql2_schema.rb +21 -0
  107. data/test/schema/postgis_schema.rb +3 -0
  108. data/test/schema/postgresql_schema.rb +63 -0
  109. data/test/schema/sqlite3_schema.rb +15 -0
  110. data/test/schema/version.rb +2 -0
  111. data/test/sqlite3/import_test.rb +4 -50
  112. data/test/support/active_support/test_case_extensions.rb +8 -1
  113. data/test/support/assertions.rb +2 -0
  114. data/test/support/factories.rb +17 -8
  115. data/test/support/generate.rb +10 -8
  116. data/test/support/mysql/import_examples.rb +17 -3
  117. data/test/support/postgresql/import_examples.rb +442 -9
  118. data/test/support/shared_examples/on_duplicate_key_ignore.rb +45 -0
  119. data/test/support/shared_examples/on_duplicate_key_update.rb +278 -1
  120. data/test/support/shared_examples/recursive_import.rb +137 -12
  121. data/test/support/sqlite3/import_examples.rb +232 -0
  122. data/test/synchronize_test.rb +10 -0
  123. data/test/test_helper.rb +44 -3
  124. data/test/value_sets_bytes_parser_test.rb +15 -2
  125. data/test/value_sets_records_parser_test.rb +2 -0
  126. metadata +74 -22
  127. data/.travis.yml +0 -52
  128. data/gemfiles/3.2.gemfile +0 -3
  129. data/gemfiles/4.0.gemfile +0 -3
  130. data/gemfiles/4.1.gemfile +0 -3
  131. data/test/schema/mysql_schema.rb +0 -16
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ostruct"
2
4
 
3
5
  module ActiveRecord::Import::ConnectionAdapters; end
4
6
 
5
7
  module ActiveRecord::Import #:nodoc:
6
- Result = Struct.new(:failed_instances, :num_inserts, :ids)
8
+ Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
7
9
 
8
10
  module ImportSupport #:nodoc:
9
11
  def supports_import? #:nodoc:
@@ -22,16 +24,110 @@ module ActiveRecord::Import #:nodoc:
22
24
  super "Missing column for value <#{name}> at index #{index}"
23
25
  end
24
26
  end
27
+
28
+ class Validator
29
+ def initialize(klass, options = {})
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
73
+ end
74
+
75
+ def valid_model?(model)
76
+ init_validations(model.class) unless model.class == @validator_class
77
+
78
+ validation_context = @options[:validate_with_context]
79
+ validation_context ||= (model.new_record? ? :create : :update)
80
+ current_context = model.send(:validation_context)
81
+
82
+ begin
83
+ model.send(:validation_context=, validation_context)
84
+ model.errors.clear
85
+
86
+ model.run_callbacks(:validation) do
87
+ if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
88
+ runner = @validate_callbacks.compile
89
+ env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
90
+ if runner.respond_to?(:call) # ActiveRecord < 5.1
91
+ runner.call(env)
92
+ else # ActiveRecord 5.1
93
+ # Note that this is a gross simplification of ActiveSupport::Callbacks#run_callbacks.
94
+ # It's technically possible for there to exist an "around" callback in the
95
+ # :validate chain, but this would be an aberration, since Rails doesn't define
96
+ # "around_validate". Still, rather than silently ignoring such callbacks, we
97
+ # explicitly raise a RuntimeError, since activerecord-import was asked to perform
98
+ # validations and it's unable to do so.
99
+ #
100
+ # The alternative here would be to copy-and-paste the bulk of the
101
+ # ActiveSupport::Callbacks#run_callbacks method, which is undesirable if there's
102
+ # no real-world use case for it.
103
+ raise "The :validate callback chain contains an 'around' callback, which is unsupported" unless runner.final?
104
+ runner.invoke_before(env)
105
+ runner.invoke_after(env)
106
+ end
107
+ elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
108
+ model.instance_eval @validate_callbacks.compile
109
+ else # ActiveRecord 3.x
110
+ model.instance_eval @validate_callbacks.compile(nil, model)
111
+ end
112
+ end
113
+
114
+ model.errors.empty?
115
+ ensure
116
+ model.send(:validation_context=, current_context)
117
+ end
118
+ end
119
+ end
25
120
  end
26
121
 
27
122
  class ActiveRecord::Associations::CollectionProxy
28
- def import(*args, &block)
29
- @association.import(*args, &block)
123
+ def bulk_import(*args, &block)
124
+ @association.bulk_import(*args, &block)
30
125
  end
126
+ alias import bulk_import unless respond_to? :import
31
127
  end
32
128
 
33
129
  class ActiveRecord::Associations::CollectionAssociation
34
- def import(*args, &block)
130
+ def bulk_import(*args, &block)
35
131
  unless owner.persisted?
36
132
  raise ActiveRecord::RecordNotSaved, "You cannot call import unless the parent is saved"
37
133
  end
@@ -40,16 +136,21 @@ class ActiveRecord::Associations::CollectionAssociation
40
136
 
41
137
  model_klass = reflection.klass
42
138
  symbolized_foreign_key = reflection.foreign_key.to_sym
43
- symbolized_column_names = model_klass.column_names.map(&:to_sym)
44
139
 
45
- owner_primary_key = owner.class.primary_key
140
+ symbolized_column_names = if model_klass.connection.respond_to?(:supports_virtual_columns?) && model_klass.connection.supports_virtual_columns?
141
+ model_klass.columns.reject(&:virtual?).map { |c| c.name.to_sym }
142
+ else
143
+ model_klass.column_names.map(&:to_sym)
144
+ end
145
+
146
+ owner_primary_key = reflection.active_record_primary_key.to_sym
46
147
  owner_primary_key_value = owner.send(owner_primary_key)
47
148
 
48
149
  # assume array of model objects
49
150
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
50
151
  if args.length == 2
51
152
  models = args.last
52
- column_names = args.first
153
+ column_names = args.first.dup
53
154
  else
54
155
  models = args.first
55
156
  column_names = symbolized_column_names
@@ -64,7 +165,46 @@ class ActiveRecord::Associations::CollectionAssociation
64
165
  m.public_send "#{reflection.type}=", owner.class.name if reflection.type
65
166
  end
66
167
 
67
- return model_klass.import column_names, models, options
168
+ return model_klass.bulk_import column_names, models, options
169
+
170
+ # supports array of hash objects
171
+ elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
172
+ if args.length == 2
173
+ array_of_hashes = args.last
174
+ column_names = args.first.dup
175
+ allow_extra_hash_keys = true
176
+ else
177
+ array_of_hashes = args.first
178
+ column_names = array_of_hashes.first.keys
179
+ allow_extra_hash_keys = false
180
+ end
181
+
182
+ symbolized_column_names = column_names.map(&:to_sym)
183
+ unless symbolized_column_names.include?(symbolized_foreign_key)
184
+ column_names << symbolized_foreign_key
185
+ end
186
+
187
+ if reflection.type && !symbolized_column_names.include?(reflection.type.to_sym)
188
+ column_names << reflection.type.to_sym
189
+ end
190
+
191
+ array_of_attributes = array_of_hashes.map do |h|
192
+ error_message = model_klass.send(:validate_hash_import, h, symbolized_column_names, allow_extra_hash_keys)
193
+
194
+ raise ArgumentError, error_message if error_message
195
+
196
+ column_names.map do |key|
197
+ if key == symbolized_foreign_key
198
+ owner_primary_key_value
199
+ elsif reflection.type && key == reflection.type.to_sym
200
+ owner.class.name
201
+ else
202
+ h[key]
203
+ end
204
+ end
205
+ end
206
+
207
+ return model_klass.bulk_import column_names, array_of_attributes, options
68
208
 
69
209
  # supports empty array
70
210
  elsif args.last.is_a?( Array ) && args.last.empty?
@@ -73,7 +213,12 @@ class ActiveRecord::Associations::CollectionAssociation
73
213
  # supports 2-element array and array
74
214
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
75
215
  column_names, array_of_attributes = args
76
- symbolized_column_names = column_names.map(&:to_s)
216
+
217
+ # dup the passed args so we don't modify unintentionally
218
+ column_names = column_names.dup
219
+ array_of_attributes = array_of_attributes.map(&:dup)
220
+
221
+ symbolized_column_names = column_names.map(&:to_sym)
77
222
 
78
223
  if symbolized_column_names.include?(symbolized_foreign_key)
79
224
  index = symbolized_column_names.index(symbolized_foreign_key)
@@ -84,31 +229,35 @@ class ActiveRecord::Associations::CollectionAssociation
84
229
  end
85
230
 
86
231
  if reflection.type
87
- column_names << reflection.type
88
- array_of_attributes.each { |attrs| attrs << owner.class.name }
232
+ symbolized_type = reflection.type.to_sym
233
+ if symbolized_column_names.include?(symbolized_type)
234
+ index = symbolized_column_names.index(symbolized_type)
235
+ array_of_attributes.each { |attrs| attrs[index] = owner.class.name }
236
+ else
237
+ column_names << symbolized_type
238
+ array_of_attributes.each { |attrs| attrs << owner.class.name }
239
+ end
89
240
  end
90
241
 
91
- return model_klass.import column_names, array_of_attributes, options
242
+ return model_klass.bulk_import column_names, array_of_attributes, options
92
243
  else
93
244
  raise ArgumentError, "Invalid arguments!"
94
245
  end
95
246
  end
247
+ alias import bulk_import unless respond_to? :import
248
+ end
249
+
250
+ module ActiveRecord::Import::Connection
251
+ def establish_connection(args = nil)
252
+ conn = super(args)
253
+ ActiveRecord::Import.load_from_connection_pool connection_pool
254
+ conn
255
+ end
96
256
  end
97
257
 
98
258
  class ActiveRecord::Base
99
259
  class << self
100
- # use tz as set in ActiveRecord::Base
101
- tproc = lambda do
102
- ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
103
- end
104
-
105
- AREXT_RAILS_COLUMNS = {
106
- create: { "created_on" => tproc,
107
- "created_at" => tproc },
108
- update: { "updated_on" => tproc,
109
- "updated_at" => tproc }
110
- }.freeze
111
- AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
260
+ prepend ActiveRecord::Import::Connection
112
261
 
113
262
  # Returns true if the current database connection adapter
114
263
  # supports import functionality, otherwise returns false.
@@ -120,14 +269,14 @@ class ActiveRecord::Base
120
269
  # supports on duplicate key update functionality, otherwise
121
270
  # returns false.
122
271
  def supports_on_duplicate_key_update?
123
- connection.supports_on_duplicate_key_update?
272
+ connection.respond_to?(:supports_on_duplicate_key_update?) && connection.supports_on_duplicate_key_update?
124
273
  end
125
274
 
126
275
  # returns true if the current database connection adapter
127
276
  # supports setting the primary key of bulk imported models, otherwise
128
277
  # returns false
129
- def support_setting_primary_key_of_imported_objects?
130
- connection.respond_to?(:support_setting_primary_key_of_imported_objects?) && connection.support_setting_primary_key_of_imported_objects?
278
+ def supports_setting_primary_key_of_imported_objects?
279
+ connection.respond_to?(:supports_setting_primary_key_of_imported_objects?) && connection.supports_setting_primary_key_of_imported_objects?
131
280
  end
132
281
 
133
282
  # Imports a collection of values to the database.
@@ -144,6 +293,9 @@ class ActiveRecord::Base
144
293
  #
145
294
  # == Usage
146
295
  # Model.import array_of_models
296
+ # Model.import column_names, array_of_models
297
+ # Model.import array_of_hash_objects
298
+ # Model.import column_names, array_of_hash_objects
147
299
  # Model.import column_names, array_of_values
148
300
  # Model.import column_names, array_of_values, options
149
301
  #
@@ -170,15 +322,21 @@ class ActiveRecord::Base
170
322
  #
171
323
  # == Options
172
324
  # * +validate+ - true|false, tells import whether or not to use
173
- # ActiveRecord validations. Validations are enforced by default.
174
- # * +ignore+ - true|false, tells import to use MySQL's INSERT IGNORE
175
- # to discard records that contain duplicate keys.
176
- # * +on_duplicate_key_ignore+ - true|false, tells import to use
177
- # Postgres 9.5+ ON CONFLICT DO NOTHING. Cannot be enabled on a
178
- # recursive import.
179
- # * +on_duplicate_key_update+ - an Array or Hash, tells import to
180
- # use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
181
- # DO UPDATE ability. See On Duplicate Key Update below.
325
+ # ActiveRecord validations. Validations are enforced by default.
326
+ # It skips the uniqueness validation for performance reasons.
327
+ # You can find more details here:
328
+ # https://github.com/zdennis/activerecord-import/issues/228
329
+ # * +ignore+ - true|false, an alias for on_duplicate_key_ignore.
330
+ # * +on_duplicate_key_ignore+ - true|false, tells import to discard
331
+ # records that contain duplicate keys. For Postgres 9.5+ it adds
332
+ # ON CONFLICT DO NOTHING, for MySQL it uses INSERT IGNORE, and for
333
+ # SQLite it uses INSERT OR IGNORE. Cannot be enabled on a
334
+ # recursive import. For database adapters that normally support
335
+ # setting primary keys on imported objects, this option prevents
336
+ # that from occurring.
337
+ # * +on_duplicate_key_update+ - :all, an Array, or Hash, tells import to
338
+ # use MySQL's ON DUPLICATE KEY UPDATE or Postgres/SQLite ON CONFLICT
339
+ # DO UPDATE ability. See On Duplicate Key Update below.
182
340
  # * +synchronize+ - an array of ActiveRecord instances for the model
183
341
  # that you are currently importing data into. This synchronizes
184
342
  # existing model instances in memory with updates from the import.
@@ -186,7 +344,7 @@ class ActiveRecord::Base
186
344
  # (if false) even if record timestamps is disabled in ActiveRecord::Base
187
345
  # * +recursive+ - true|false, tells import to import all has_many/has_one
188
346
  # associations if the adapter supports setting the primary keys of the
189
- # newly imported objects.
347
+ # newly imported objects. PostgreSQL only.
190
348
  # * +batch_size+ - an integer value to specify the max number of records to
191
349
  # include per insert. Defaults to the total number of records to import.
192
350
  #
@@ -199,6 +357,18 @@ class ActiveRecord::Base
199
357
  # BlogPost.new author_name: 'Zach Dennis', title: 'AREXT3' ]
200
358
  # BlogPost.import posts
201
359
  #
360
+ # # Example using array_of_hash_objects
361
+ # # NOTE: column_names will be determined by using the keys of the first hash in the array. If later hashes in the
362
+ # # array have different keys an exception will be raised. If you have hashes to import with different sets of keys
363
+ # # we recommend grouping these into batches before importing.
364
+ # values = [ {author_name: 'zdennis', title: 'test post'} ], [ {author_name: 'jdoe', title: 'another test post'} ] ]
365
+ # BlogPost.import values
366
+ #
367
+ # # Example using column_names and array_of_hash_objects
368
+ # columns = [ :author_name, :title ]
369
+ # values = [ {author_name: 'zdennis', title: 'test post'} ], [ {author_name: 'jdoe', title: 'another test post'} ] ]
370
+ # BlogPost.import columns, values
371
+ #
202
372
  # # Example using column_names and array_of_values
203
373
  # columns = [ :author_name, :title ]
204
374
  # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
@@ -224,7 +394,15 @@ class ActiveRecord::Base
224
394
  #
225
395
  # == On Duplicate Key Update (MySQL)
226
396
  #
227
- # The :on_duplicate_key_update option can be either an Array or a Hash.
397
+ # The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
398
+ #
399
+ # ==== Using :all
400
+ #
401
+ # The :on_duplicate_key_update option can be set to :all. All columns
402
+ # other than the primary key are updated. If a list of column names is
403
+ # supplied, only those columns will be updated. Below is an example:
404
+ #
405
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
228
406
  #
229
407
  # ==== Using an Array
230
408
  #
@@ -243,10 +421,19 @@ class ActiveRecord::Base
243
421
  #
244
422
  # BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
245
423
  #
246
- # == On Duplicate Key Update (Postgres 9.5+)
424
+ # == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
425
+ #
426
+ # The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
427
+ # three attributes, :conflict_target (and optionally :index_predicate) or
428
+ # :constraint_name (Postgres), and :columns.
429
+ #
430
+ # ==== Using :all
247
431
  #
248
- # The :on_duplicate_key_update option can be an Array or a Hash with up to
249
- # two attributes, :conflict_target or :constraint_name and :columns.
432
+ # The :on_duplicate_key_update option can be set to :all. All columns
433
+ # other than the primary key are updated. If a list of column names is
434
+ # supplied, only those columns will be updated. Below is an example:
435
+ #
436
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
250
437
  #
251
438
  # ==== Using an Array
252
439
  #
@@ -260,12 +447,13 @@ class ActiveRecord::Base
260
447
  #
261
448
  # ==== Using a Hash
262
449
  #
263
- # The :on_duplicate_update option can be a hash with up to two attributes,
264
- # :conflict_target or constraint_name, and :columns. Unlike MySQL, Postgres
265
- # requires the conflicting constraint to be explicitly specified. Using this
266
- # option allows you to specify a constraint other than the primary key.
450
+ # The :on_duplicate_key_update option can be a hash with up to three
451
+ # attributes, :conflict_target (and optionally :index_predicate) or
452
+ # :constraint_name, and :columns. Unlike MySQL, Postgres requires the
453
+ # conflicting constraint to be explicitly specified. Using this option
454
+ # allows you to specify a constraint other than the primary key.
267
455
  #
268
- # ====== :conflict_target
456
+ # ===== :conflict_target
269
457
  #
270
458
  # The :conflict_target attribute specifies the columns that make up the
271
459
  # conflicting unique constraint and can be a single column or an array of
@@ -273,9 +461,18 @@ class ActiveRecord::Base
273
461
  # but it is the preferred method of identifying a constraint. It will
274
462
  # default to the primary key. Below is an example:
275
463
  #
276
- # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [:author_id, :slug], columns: [ :date_modified ] }
464
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], columns: [ :date_modified ] }
465
+ #
466
+ # ===== :index_predicate
467
+ #
468
+ # The :index_predicate attribute optionally specifies a WHERE condition
469
+ # on :conflict_target, which is required for matching against partial
470
+ # indexes. This attribute is ignored if :constraint_name is included.
471
+ # Below is an example:
472
+ #
473
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], index_predicate: 'status <> 0', columns: [ :date_modified ] }
277
474
  #
278
- # ====== :constraint_name
475
+ # ===== :constraint_name
279
476
  #
280
477
  # The :constraint_name attribute explicitly identifies the conflicting
281
478
  # unique index by name. Postgres documentation discourages using this method
@@ -283,11 +480,28 @@ class ActiveRecord::Base
283
480
  #
284
481
  # BlogPost.import columns, values, on_duplicate_key_update: { constraint_name: :blog_posts_pkey, columns: [ :date_modified ] }
285
482
  #
286
- # ====== :columns
483
+ # ===== :condition
484
+ #
485
+ # The :condition attribute optionally specifies a WHERE condition
486
+ # on :conflict_action. Only rows for which this expression returns true will be updated.
487
+ # Note that it's evaluated last, after a conflict has been identified as a candidate to update.
488
+ # Below is an example:
489
+ #
490
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id ], condition: "blog_posts.title NOT LIKE '%sample%'", columns: [ :author_name ] }
491
+ #
492
+ # ===== :columns
287
493
  #
288
- # The :columns attribute can be either an Array or a Hash.
494
+ # The :columns attribute can be either :all, an Array, or a Hash.
289
495
  #
290
- # ======== Using an Array
496
+ # ===== Using :all
497
+ #
498
+ # The :columns attribute can be :all. All columns other than the primary key will be updated.
499
+ # If a list of column names is supplied, only those columns will be updated.
500
+ # Below is an example:
501
+ #
502
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
503
+ #
504
+ # ===== Using an Array
291
505
  #
292
506
  # The :columns attribute can be an array of column names. The column names
293
507
  # are the only fields that are updated if a duplicate record is found.
@@ -295,7 +509,7 @@ class ActiveRecord::Base
295
509
  #
296
510
  # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: [ :date_modified, :content, :author ] }
297
511
  #
298
- # ======== Using a Hash
512
+ # ===== Using a Hash
299
513
  #
300
514
  # The :columns option can be a hash of column names to model attribute name
301
515
  # mappings. This gives you finer grained control over what fields are updated
@@ -307,8 +521,9 @@ class ActiveRecord::Base
307
521
  # This returns an object which responds to +failed_instances+ and +num_inserts+.
308
522
  # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
309
523
  # * num_inserts - the number of insert statements it took to import the data
310
- # * ids - the primary keys of the imported ids, if the adpater supports it, otherwise and empty array.
311
- def import(*args)
524
+ # * ids - the primary keys of the imported ids if the adapter supports it, otherwise an empty array.
525
+ # * results - import results if the adapter supports it, otherwise an empty array.
526
+ def bulk_import(*args)
312
527
  if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
313
528
  options = {}
314
529
  options.merge!( args.pop ) if args.last.is_a?(Hash)
@@ -319,115 +534,217 @@ class ActiveRecord::Base
319
534
  import_helper(*args)
320
535
  end
321
536
  end
537
+ alias import bulk_import unless ActiveRecord::Base.respond_to? :import
322
538
 
323
539
  # Imports a collection of values if all values are valid. Import fails at the
324
540
  # first encountered validation error and raises ActiveRecord::RecordInvalid
325
541
  # with the failed instance.
326
- def import!(*args)
542
+ def bulk_import!(*args)
327
543
  options = args.last.is_a?( Hash ) ? args.pop : {}
328
544
  options[:validate] = true
329
545
  options[:raise_error] = true
330
546
 
331
- import(*args, options)
547
+ bulk_import(*args, options)
332
548
  end
549
+ alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
333
550
 
334
551
  def import_helper( *args )
335
- options = { validate: true, timestamps: true, primary_key: primary_key }
552
+ options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
336
553
  options.merge!( args.pop ) if args.last.is_a? Hash
554
+ # making sure that current model's primary key is used
555
+ options[:primary_key] = primary_key
556
+ options[:locking_column] = locking_column if attribute_names.include?(locking_column)
337
557
 
338
- # Don't modify incoming arguments
339
- if options[:on_duplicate_key_update]
340
- options[:on_duplicate_key_update] = options[:on_duplicate_key_update].dup
341
- end
342
-
343
- is_validating = options[:validate]
344
- is_validating = true unless options[:validate_with_context].nil?
558
+ is_validating = options[:validate_with_context].present? ? true : options[:validate]
559
+ validator = ActiveRecord::Import::Validator.new(self, options)
345
560
 
346
561
  # assume array of model objects
347
562
  if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
348
563
  if args.length == 2
349
564
  models = args.last
350
- column_names = args.first
565
+ column_names = args.first.dup
351
566
  else
352
567
  models = args.first
353
- column_names = self.column_names.dup
568
+ column_names = if connection.respond_to?(:supports_virtual_columns?) && connection.supports_virtual_columns?
569
+ columns.reject(&:virtual?).map(&:name)
570
+ else
571
+ self.column_names.dup
572
+ end
573
+ end
574
+
575
+ if models.first.id.nil?
576
+ Array(primary_key).each do |c|
577
+ if column_names.include?(c) && columns_hash[c].type == :uuid
578
+ column_names.delete(c)
579
+ end
580
+ end
354
581
  end
355
582
 
356
- array_of_attributes = models.map do |model|
357
- # this next line breaks sqlite.so with a segmentation fault
358
- # if model.new_record? || options[:on_duplicate_key_update]
359
- column_names.map do |name|
360
- model.read_attribute_before_type_cast(name.to_s)
583
+ update_attrs = if record_timestamps && options[:timestamps]
584
+ if respond_to?(:timestamp_attributes_for_update, true)
585
+ send(:timestamp_attributes_for_update).map(&:to_sym)
586
+ else
587
+ allocate.send(:timestamp_attributes_for_update_in_model)
588
+ end
589
+ end
590
+
591
+ array_of_attributes = []
592
+
593
+ models.each do |model|
594
+ if supports_setting_primary_key_of_imported_objects?
595
+ load_association_ids(model)
596
+ end
597
+
598
+ if is_validating && !validator.valid_model?(model)
599
+ raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
600
+ next
601
+ end
602
+
603
+ array_of_attributes << column_names.map do |name|
604
+ if model.persisted? &&
605
+ update_attrs && update_attrs.include?(name.to_sym) &&
606
+ !model.send("#{name}_changed?")
607
+ nil
608
+ else
609
+ model.read_attribute(name.to_s)
610
+ end
611
+ end
612
+ end
613
+ # supports array of hash objects
614
+ elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
615
+ if args.length == 2
616
+ array_of_hashes = args.last
617
+ column_names = args.first.dup
618
+ allow_extra_hash_keys = true
619
+ else
620
+ array_of_hashes = args.first
621
+ column_names = array_of_hashes.first.keys
622
+ allow_extra_hash_keys = false
623
+ end
624
+
625
+ array_of_attributes = array_of_hashes.map do |h|
626
+ error_message = validate_hash_import(h, column_names, allow_extra_hash_keys)
627
+
628
+ raise ArgumentError, error_message if error_message
629
+
630
+ column_names.map do |key|
631
+ h[key]
361
632
  end
362
- # end
363
633
  end
364
634
  # supports empty array
365
635
  elsif args.last.is_a?( Array ) && args.last.empty?
366
- return ActiveRecord::Import::Result.new([], 0, [])
636
+ return ActiveRecord::Import::Result.new([], 0, [], [])
367
637
  # supports 2-element array and array
368
638
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
639
+
640
+ unless args.last.first.is_a?(Array)
641
+ raise ArgumentError, "Last argument should be a two dimensional array '[[]]'. First element in array was a #{args.last.first.class}"
642
+ end
643
+
369
644
  column_names, array_of_attributes = args
645
+
646
+ # dup the passed args so we don't modify unintentionally
647
+ column_names = column_names.dup
648
+ array_of_attributes = array_of_attributes.map(&:dup)
370
649
  else
371
650
  raise ArgumentError, "Invalid arguments!"
372
651
  end
373
652
 
374
- # dup the passed in array so we don't modify it unintentionally
375
- column_names = column_names.dup
376
- array_of_attributes = array_of_attributes.dup
377
-
378
653
  # Force the primary key col into the insert if it's not
379
654
  # on the list and we are using a sequence and stuff a nil
380
655
  # value for it into each row so the sequencer will fire later
381
- if !column_names.include?(primary_key) && connection.prefetch_primary_key? && sequence_name
382
- column_names << primary_key
383
- array_of_attributes.each { |a| a << nil }
656
+ symbolized_column_names = Array(column_names).map(&:to_sym)
657
+ symbolized_primary_key = Array(primary_key).map(&:to_sym)
658
+
659
+ if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && connection.prefetch_primary_key? && sequence_name
660
+ column_count = column_names.size
661
+ column_names.concat(Array(primary_key)).uniq!
662
+ columns_added = column_names.size - column_count
663
+ new_fields = Array.new(columns_added)
664
+ array_of_attributes.each { |a| a.concat(new_fields) }
384
665
  end
385
666
 
667
+ # Don't modify incoming arguments
668
+ on_duplicate_key_update = options[:on_duplicate_key_update]
669
+ if on_duplicate_key_update
670
+ updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
671
+ options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
672
+ on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
673
+ duped_options[k] = if k == :columns && v == :all
674
+ updatable_columns
675
+ elsif v.duplicable?
676
+ v.dup
677
+ else
678
+ v
679
+ end
680
+ end
681
+ elsif on_duplicate_key_update == :all
682
+ updatable_columns
683
+ elsif on_duplicate_key_update.duplicable?
684
+ on_duplicate_key_update.dup
685
+ else
686
+ on_duplicate_key_update
687
+ end
688
+ end
689
+
690
+ timestamps = {}
691
+
386
692
  # record timestamps unless disabled in ActiveRecord::Base
387
- if record_timestamps && options.delete( :timestamps )
388
- add_special_rails_stamps column_names, array_of_attributes, options
693
+ if record_timestamps && options[:timestamps]
694
+ timestamps = add_special_rails_stamps column_names, array_of_attributes, options
389
695
  end
390
696
 
391
697
  return_obj = if is_validating
392
- if models
393
- import_with_validations( column_names, array_of_attributes, options ) do |failed|
394
- models.each_with_index do |model, i|
395
- model = model.dup if options[:recursive]
396
- next if model.valid?(options[:validate_with_context])
397
- model.send(:raise_record_invalid) if options[:raise_error]
698
+ import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
699
+ if models
700
+ models.each { |m| failed_instances << m if m.errors.any? }
701
+ else
702
+ # create instances for each of our column/value sets
703
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
704
+
705
+ # keep track of the instance and the position it is currently at. if this fails
706
+ # validation we'll use the index to remove it from the array_of_attributes
707
+ arr.each_with_index do |hsh, i|
708
+ # utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
709
+ model = new do |m|
710
+ hsh.each_pair { |k, v| m[k] = v }
711
+ end
712
+
713
+ next if validator.valid_model?(model)
714
+ raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
715
+
398
716
  array_of_attributes[i] = nil
399
- failed << model
717
+ failure = model.dup
718
+ failure.errors.send(:initialize_dup, model.errors)
719
+ failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
400
720
  end
721
+ array_of_attributes.compact!
401
722
  end
402
- else
403
- import_with_validations( column_names, array_of_attributes, options )
404
723
  end
405
724
  else
406
- (num_inserts, ids) = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
407
- ActiveRecord::Import::Result.new([], num_inserts, ids)
725
+ import_without_validations_or_callbacks( column_names, array_of_attributes, options )
408
726
  end
409
727
 
410
728
  if options[:synchronize]
411
- sync_keys = options[:synchronize_keys] || [primary_key]
729
+ sync_keys = options[:synchronize_keys] || Array(primary_key)
412
730
  synchronize( options[:synchronize], sync_keys)
413
731
  end
414
732
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
415
733
 
416
734
  # if we have ids, then set the id on the models and mark the models as clean.
417
- if support_setting_primary_key_of_imported_objects?
418
- set_ids_and_mark_clean(models, return_obj)
735
+ if models && supports_setting_primary_key_of_imported_objects?
736
+ set_attributes_and_mark_clean(models, return_obj, timestamps, options)
419
737
 
420
738
  # if there are auto-save associations on the models we imported that are new, import them as well
421
- import_associations(models, options.dup) if options[:recursive]
739
+ if options[:recursive]
740
+ options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
741
+ import_associations(models, options.dup.merge(validate: false))
742
+ end
422
743
  end
423
744
 
424
745
  return_obj
425
746
  end
426
747
 
427
- # TODO import_from_table needs to be implemented.
428
- def import_from_table( options ) # :nodoc:
429
- end
430
-
431
748
  # Imports the passed in +column_names+ and +array_of_attributes+
432
749
  # given the passed in +options+ Hash with validations. Returns an
433
750
  # object with the methods +failed_instances+ and +num_inserts+.
@@ -438,32 +755,14 @@ class ActiveRecord::Base
438
755
  def import_with_validations( column_names, array_of_attributes, options = {} )
439
756
  failed_instances = []
440
757
 
441
- if block_given?
442
- yield failed_instances
443
- else
444
- # create instances for each of our column/value sets
445
- arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
446
-
447
- # keep track of the instance and the position it is currently at. if this fails
448
- # validation we'll use the index to remove it from the array_of_attributes
449
- model = new
450
- arr.each_with_index do |hsh, i|
451
- hsh.each_pair { |k, v| model[k] = v }
452
- next if model.valid?(options[:validate_with_context])
453
- raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
454
- array_of_attributes[i] = nil
455
- failed_instances << model.dup
456
- end
457
- end
458
-
459
- array_of_attributes.compact!
758
+ yield failed_instances if block_given?
460
759
 
461
- num_inserts, ids = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
462
- [0, []]
760
+ result = if options[:all_or_none] && failed_instances.any?
761
+ ActiveRecord::Import::Result.new([], 0, [], [])
463
762
  else
464
763
  import_without_validations_or_callbacks( column_names, array_of_attributes, options )
465
764
  end
466
- ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids)
765
+ ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results)
467
766
  end
468
767
 
469
768
  # Imports the passed in +column_names+ and +array_of_attributes+
@@ -473,66 +772,143 @@ class ActiveRecord::Base
473
772
  # information on +column_names+, +array_of_attributes_ and
474
773
  # +options+.
475
774
  def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
775
+ return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
776
+
476
777
  column_names = column_names.map(&:to_sym)
477
778
  scope_columns, scope_values = scope_attributes.to_a.transpose
478
779
 
479
780
  unless scope_columns.blank?
480
781
  scope_columns.zip(scope_values).each do |name, value|
481
782
  name_as_sym = name.to_sym
482
- next if column_names.include?(name_as_sym)
483
-
484
- is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
485
- value = value.first if is_sti
486
-
783
+ next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
487
784
  column_names << name_as_sym
488
785
  array_of_attributes.each { |attrs| attrs << value }
489
786
  end
490
787
  end
491
788
 
789
+ if finder_needs_type_condition?
790
+ unless column_names.include?(inheritance_column.to_sym)
791
+ column_names << inheritance_column.to_sym
792
+ array_of_attributes.each { |attrs| attrs << sti_name }
793
+ end
794
+ end
795
+
492
796
  columns = column_names.each_with_index.map do |name, i|
493
797
  column = columns_hash[name.to_s]
494
-
495
798
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
496
-
497
799
  column
498
800
  end
499
801
 
500
802
  columns_sql = "(#{column_names.map { |name| connection.quote_column_name(name) }.join(',')})"
501
- insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ' : ''}INTO #{quoted_table_name} #{columns_sql} VALUES "
803
+ pre_sql_statements = connection.pre_sql_statements( options )
804
+ insert_sql = ['INSERT', pre_sql_statements, "INTO #{quoted_table_name} #{columns_sql} VALUES "]
805
+ insert_sql = insert_sql.flatten.join(' ')
502
806
  values_sql = values_sql_for_columns_and_attributes(columns, array_of_attributes)
503
807
 
504
808
  number_inserted = 0
505
809
  ids = []
810
+ results = []
506
811
  if supports_import?
507
812
  # generate the sql
508
813
  post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
814
+ import_size = values_sql.size
815
+
816
+ batch_size = options[:batch_size] || import_size
817
+ run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
818
+ progress_proc = options[:batch_progress]
819
+ current_batch = 0
820
+ batches = (import_size / batch_size.to_f).ceil
509
821
 
510
- batch_size = options[:batch_size] || values_sql.size
511
822
  values_sql.each_slice(batch_size) do |batch_values|
823
+ batch_started_at = Time.now.to_i
824
+
512
825
  # perform the inserts
513
826
  result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
514
827
  batch_values,
515
- "#{self.class.name} Create Many Without Validations Or Callbacks" )
516
- number_inserted += result[0]
517
- ids += result[1]
828
+ options,
829
+ "#{model_name} Create Many" )
830
+
831
+ number_inserted += result.num_inserts
832
+ ids += result.ids
833
+ results += result.results
834
+ current_batch += 1
835
+
836
+ progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
518
837
  end
519
838
  else
520
- values_sql.each do |values|
521
- ids << connection.insert(insert_sql + values)
522
- number_inserted += 1
839
+ transaction(requires_new: true) do
840
+ values_sql.each do |values|
841
+ ids << connection.insert(insert_sql + values)
842
+ number_inserted += 1
843
+ end
523
844
  end
524
845
  end
525
- [number_inserted, ids]
846
+ ActiveRecord::Import::Result.new([], number_inserted, ids, results)
526
847
  end
527
848
 
528
849
  private
529
850
 
530
- def set_ids_and_mark_clean(models, import_result)
851
+ def set_attributes_and_mark_clean(models, import_result, timestamps, options)
531
852
  return if models.nil?
532
- import_result.ids.each_with_index do |id, index|
533
- model = models[index]
534
- model.id = id.to_i
535
- if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
853
+ models -= import_result.failed_instances
854
+
855
+ # if ids were returned for all models we know all were updated
856
+ if models.size == import_result.ids.size
857
+ import_result.ids.each_with_index do |id, index|
858
+ model = models[index]
859
+ model.id = id
860
+
861
+ timestamps.each do |attr, value|
862
+ model.send(attr + "=", value) if model.send(attr).nil?
863
+ end
864
+ end
865
+ end
866
+
867
+ deserialize_value = lambda do |column, value|
868
+ column = columns_hash[column]
869
+ return value unless column
870
+ if respond_to?(:type_caster)
871
+ type = type_for_attribute(column.name)
872
+ type.deserialize(value)
873
+ elsif column.respond_to?(:type_cast_from_database)
874
+ column.type_cast_from_database(value)
875
+ else
876
+ value
877
+ end
878
+ end
879
+
880
+ set_value = lambda do |model, column, value|
881
+ val = deserialize_value.call(column, value)
882
+ if model.attribute_names.include?(column)
883
+ model.send("#{column}=", val)
884
+ else
885
+ attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
886
+ model.instance_variable_set(:@attributes, attributes)
887
+ end
888
+ end
889
+
890
+ columns = Array(options[:returning_columns])
891
+ results = Array(import_result.results)
892
+ if models.size == results.size
893
+ single_column = columns.first if columns.size == 1
894
+ results.each_with_index do |result, index|
895
+ model = models[index]
896
+
897
+ if single_column
898
+ set_value.call(model, single_column, result)
899
+ else
900
+ columns.each_with_index do |column, col_index|
901
+ set_value.call(model, column, result[col_index])
902
+ end
903
+ end
904
+ end
905
+ end
906
+
907
+ models.each do |model|
908
+ if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
909
+ model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
910
+ model.changes_applied
911
+ elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
536
912
  model.clear_changes_information
537
913
  else # Rails 3.2
538
914
  model.instance_variable_get(:@changed_attributes).clear
@@ -541,20 +917,44 @@ class ActiveRecord::Base
541
917
  end
542
918
  end
543
919
 
920
+ # Sync belongs_to association ids with foreign key field
921
+ def load_association_ids(model)
922
+ changed_columns = model.changed
923
+ association_reflections = model.class.reflect_on_all_associations(:belongs_to)
924
+ association_reflections.each do |association_reflection|
925
+ next if association_reflection.options[:polymorphic]
926
+
927
+ column_names = Array(association_reflection.foreign_key).map(&:to_s)
928
+ column_names.each_with_index do |column_name, column_index|
929
+ next if changed_columns.include?(column_name)
930
+
931
+ association = model.association(association_reflection.name)
932
+ association = association.target
933
+ next if association.blank? || model.public_send(column_name).present?
934
+
935
+ association_primary_key = Array(association_reflection.association_primary_key)[column_index]
936
+ model.public_send("#{column_name}=", association.send(association_primary_key))
937
+ end
938
+ end
939
+ end
940
+
544
941
  def import_associations(models, options)
545
942
  # now, for all the dirty associations, collect them into a new set of models, then recurse.
546
943
  # notes:
547
944
  # does not handle associations that reference themselves
548
945
  # should probably take a hash to associations to follow.
946
+ return if models.nil?
549
947
  associated_objects_by_class = {}
550
948
  models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
551
949
 
552
- # :on_duplicate_key_update not supported for associations
553
- options.delete(:on_duplicate_key_update)
950
+ # :on_duplicate_key_update only supported for all fields
951
+ options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
952
+ # :returning not supported for associations
953
+ options.delete(:returning)
554
954
 
555
955
  associated_objects_by_class.each_value do |associations|
556
956
  associations.each_value do |associated_records|
557
- associated_records.first.class.import(associated_records, options) unless associated_records.empty?
957
+ associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
558
958
  end
559
959
  end
560
960
  end
@@ -563,6 +963,7 @@ class ActiveRecord::Base
563
963
  # of class => objects to import.
564
964
  def find_associated_objects_for_import(associated_objects_by_class, model)
565
965
  associated_objects_by_class[model.class.name] ||= {}
966
+ return associated_objects_by_class unless model.id
566
967
 
567
968
  association_reflections =
568
969
  model.class.reflect_on_all_associations(:has_one) +
@@ -580,8 +981,13 @@ class ActiveRecord::Base
580
981
  changed_objects.each do |child|
581
982
  child.public_send("#{association_reflection.foreign_key}=", model.id)
582
983
  # For polymorphic associations
984
+ association_name = if model.class.respond_to?(:polymorphic_name)
985
+ model.class.polymorphic_name
986
+ else
987
+ model.class.base_class
988
+ end
583
989
  association_reflection.type.try do |type|
584
- child.public_send("#{type}=", model.class.name)
990
+ child.public_send("#{type}=", association_name)
585
991
  end
586
992
  end
587
993
  associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
@@ -595,24 +1001,33 @@ class ActiveRecord::Base
595
1001
  # connection gets called a *lot* in this high intensity loop.
596
1002
  # Reuse the same one w/in the loop, otherwise it would keep being re-retreived (= lots of time for large imports)
597
1003
  connection_memo = connection
1004
+
598
1005
  array_of_attributes.map do |arr|
599
1006
  my_values = arr.each_with_index.map do |val, j|
600
1007
  column = columns[j]
601
1008
 
602
1009
  # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
603
- if val.nil? && column.name == primary_key && !sequence_name.blank?
1010
+ if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
604
1011
  connection_memo.next_value_for_sequence(sequence_name)
1012
+ elsif val.respond_to?(:to_sql)
1013
+ "(#{val.to_sql})"
605
1014
  elsif column
606
- if respond_to?(:type_caster) && type_caster.respond_to?(:type_cast_for_database) # Rails 5.0 and higher
607
- connection_memo.quote(type_caster.type_cast_for_database(column.name, val))
608
- elsif column.respond_to?(:type_cast_from_user) # Rails 4.2 and higher
1015
+ if respond_to?(:type_caster) # Rails 5.0 and higher
1016
+ type = type_for_attribute(column.name)
1017
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
1018
+ connection_memo.quote(val)
1019
+ elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
609
1020
  connection_memo.quote(column.type_cast_from_user(val), column)
610
- else # Rails 3.2, 4.0 and 4.1
1021
+ else # Rails 3.2, 4.0 and 4.1
611
1022
  if serialized_attributes.include?(column.name)
612
1023
  val = serialized_attributes[column.name].dump(val)
613
1024
  end
614
- connection_memo.quote(column.type_cast(val), column)
1025
+ # Fixes #443 to support binary (i.e. bytea) columns on PG
1026
+ val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
1027
+ connection_memo.quote(val, column)
615
1028
  end
1029
+ else
1030
+ raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
616
1031
  end
617
1032
  end
618
1033
  "(#{my_values.join(',')})"
@@ -620,40 +1035,89 @@ class ActiveRecord::Base
620
1035
  end
621
1036
 
622
1037
  def add_special_rails_stamps( column_names, array_of_attributes, options )
623
- AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
624
- next unless self.column_names.include?(key)
625
- value = blk.call
626
- index = column_names.index(key) || column_names.index(key.to_sym)
627
- if index
628
- # replace every instance of the array of attributes with our value
629
- array_of_attributes.each { |arr| arr[index] = value if arr[index].nil? }
630
- else
631
- column_names << key
632
- array_of_attributes.each { |arr| arr << value }
633
- end
1038
+ timestamp_columns = {}
1039
+ timestamps = {}
1040
+
1041
+ if respond_to?(:all_timestamp_attributes_in_model, true) # Rails 5.1 and higher
1042
+ timestamp_columns[:create] = timestamp_attributes_for_create_in_model
1043
+ timestamp_columns[:update] = timestamp_attributes_for_update_in_model
1044
+ else
1045
+ instance = allocate
1046
+ timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
1047
+ timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
634
1048
  end
635
1049
 
636
- AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
637
- next unless self.column_names.include?(key)
638
- value = blk.call
639
- index = column_names.index(key) || column_names.index(key.to_sym)
640
- if index
641
- # replace every instance of the array of attributes with our value
642
- array_of_attributes.each { |arr| arr[index] = value }
643
- else
644
- column_names << key
645
- array_of_attributes.each { |arr| arr << value }
646
- end
1050
+ # use tz as set in ActiveRecord::Base
1051
+ default_timezone = if ActiveRecord.respond_to?(:default_timezone)
1052
+ ActiveRecord.default_timezone
1053
+ else
1054
+ ActiveRecord::Base.default_timezone
1055
+ end
1056
+ timestamp = default_timezone == :utc ? Time.now.utc : Time.now
1057
+
1058
+ [:create, :update].each do |action|
1059
+ timestamp_columns[action].each do |column|
1060
+ column = column.to_s
1061
+ timestamps[column] = timestamp
1062
+
1063
+ index = column_names.index(column) || column_names.index(column.to_sym)
1064
+ if index
1065
+ # replace every instance of the array of attributes with our value
1066
+ array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
1067
+ else
1068
+ column_names << column
1069
+ array_of_attributes.each { |arr| arr << timestamp }
1070
+ end
647
1071
 
648
- if supports_on_duplicate_key_update?
649
- connection.add_column_for_on_duplicate_key_update(key, options)
1072
+ if supports_on_duplicate_key_update? && action == :update
1073
+ connection.add_column_for_on_duplicate_key_update(column, options)
1074
+ end
650
1075
  end
651
1076
  end
1077
+
1078
+ timestamps
652
1079
  end
653
1080
 
654
1081
  # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
655
1082
  def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
656
1083
  array_of_attributes.map { |values| Hash[column_names.zip(values)] }
657
1084
  end
1085
+
1086
+ # Checks that the imported hash has the required_keys, optionally also checks that the hash has
1087
+ # no keys beyond those required when `allow_extra_keys` is false.
1088
+ # returns `nil` if validation passes, or an error message if it fails
1089
+ def validate_hash_import(hash, required_keys, allow_extra_keys) # :nodoc:
1090
+ extra_keys = allow_extra_keys ? [] : hash.keys - required_keys
1091
+ missing_keys = required_keys - hash.keys
1092
+
1093
+ return nil if extra_keys.empty? && missing_keys.empty?
1094
+
1095
+ if allow_extra_keys
1096
+ <<-EOS
1097
+ Hash key mismatch.
1098
+
1099
+ When importing an array of hashes with provided columns_names, each hash must contain keys for all column_names.
1100
+
1101
+ Required keys: #{required_keys}
1102
+ Missing keys: #{missing_keys}
1103
+
1104
+ Hash: #{hash}
1105
+ EOS
1106
+ else
1107
+ <<-EOS
1108
+ Hash key mismatch.
1109
+
1110
+ When importing an array of hashes, all hashes must have the same keys.
1111
+ If you have records that are missing some values, we recommend you either set default values
1112
+ for the missing keys or group these records into batches by key set before importing.
1113
+
1114
+ Required keys: #{required_keys}
1115
+ Extra keys: #{extra_keys}
1116
+ Missing keys: #{missing_keys}
1117
+
1118
+ Hash: #{hash}
1119
+ EOS
1120
+ end
1121
+ end
658
1122
  end
659
1123
  end