activerecord-import 0.10.0 → 1.0.8

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