activerecord-import 0.19.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +22 -12
- data/CHANGELOG.md +166 -0
- data/Gemfile +13 -10
- data/README.markdown +548 -5
- data/Rakefile +2 -1
- data/benchmarks/lib/cli_parser.rb +2 -1
- data/gemfiles/5.1.gemfile +1 -0
- data/gemfiles/5.2.gemfile +2 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +2 -2
- data/lib/activerecord-import/adapters/mysql_adapter.rb +16 -10
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +59 -15
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +126 -3
- data/lib/activerecord-import/base.rb +4 -6
- data/lib/activerecord-import/import.rb +384 -126
- data/lib/activerecord-import/synchronize.rb +1 -1
- data/lib/activerecord-import/value_sets_parser.rb +14 -0
- data/lib/activerecord-import/version.rb +1 -1
- data/lib/activerecord-import.rb +2 -15
- data/test/adapters/makara_postgis.rb +1 -0
- data/test/import_test.rb +148 -14
- data/test/makara_postgis/import_test.rb +8 -0
- data/test/models/account.rb +3 -0
- data/test/models/bike_maker.rb +7 -0
- data/test/models/topic.rb +10 -0
- data/test/models/user.rb +3 -0
- data/test/models/user_token.rb +4 -0
- data/test/schema/generic_schema.rb +20 -0
- data/test/schema/mysql2_schema.rb +19 -0
- data/test/schema/postgresql_schema.rb +1 -0
- data/test/schema/sqlite3_schema.rb +13 -0
- data/test/support/factories.rb +9 -8
- data/test/support/generate.rb +6 -6
- data/test/support/mysql/import_examples.rb +14 -2
- data/test/support/postgresql/import_examples.rb +142 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +252 -1
- data/test/support/shared_examples/recursive_import.rb +41 -11
- data/test/support/sqlite3/import_examples.rb +187 -10
- data/test/synchronize_test.rb +8 -0
- data/test/test_helper.rb +9 -1
- data/test/value_sets_bytes_parser_test.rb +13 -2
- metadata +20 -5
- data/test/schema/mysql_schema.rb +0 -16
@@ -3,7 +3,7 @@ require "ostruct"
|
|
3
3
|
module ActiveRecord::Import::ConnectionAdapters; end
|
4
4
|
|
5
5
|
module ActiveRecord::Import #:nodoc:
|
6
|
-
Result = Struct.new(:failed_instances, :num_inserts, :ids)
|
6
|
+
Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
|
7
7
|
|
8
8
|
module ImportSupport #:nodoc:
|
9
9
|
def supports_import? #:nodoc:
|
@@ -24,27 +24,63 @@ module ActiveRecord::Import #:nodoc:
|
|
24
24
|
end
|
25
25
|
|
26
26
|
class Validator
|
27
|
-
def initialize(options = {})
|
27
|
+
def initialize(klass, options = {})
|
28
28
|
@options = options
|
29
|
+
init_validations(klass)
|
30
|
+
end
|
31
|
+
|
32
|
+
def init_validations(klass)
|
33
|
+
@validate_callbacks = klass._validate_callbacks.dup
|
34
|
+
|
35
|
+
@validate_callbacks.each_with_index do |callback, i|
|
36
|
+
filter = callback.raw_filter
|
37
|
+
next unless filter.class.name =~ /Validations::PresenceValidator/ ||
|
38
|
+
(!@options[:validate_uniqueness] &&
|
39
|
+
filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
|
40
|
+
|
41
|
+
callback = callback.dup
|
42
|
+
filter = filter.dup
|
43
|
+
attrs = filter.instance_variable_get(:@attributes).dup
|
44
|
+
|
45
|
+
if filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
46
|
+
attrs = []
|
47
|
+
else
|
48
|
+
associations = klass.reflect_on_all_associations(:belongs_to)
|
49
|
+
associations.each do |assoc|
|
50
|
+
if (index = attrs.index(assoc.name))
|
51
|
+
key = assoc.foreign_key.to_sym
|
52
|
+
attrs[index] = key unless attrs.include?(key)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
filter.instance_variable_set(:@attributes, attrs)
|
58
|
+
|
59
|
+
if @validate_callbacks.respond_to?(:chain, true)
|
60
|
+
@validate_callbacks.send(:chain).tap do |chain|
|
61
|
+
callback.instance_variable_set(:@filter, filter)
|
62
|
+
chain[i] = callback
|
63
|
+
end
|
64
|
+
else
|
65
|
+
callback.raw_filter = filter
|
66
|
+
callback.filter = callback.send(:_compile_filter, filter)
|
67
|
+
@validate_callbacks[i] = callback
|
68
|
+
end
|
69
|
+
end
|
29
70
|
end
|
30
71
|
|
31
72
|
def valid_model?(model)
|
32
73
|
validation_context = @options[:validate_with_context]
|
33
74
|
validation_context ||= (model.new_record? ? :create : :update)
|
34
|
-
|
35
75
|
current_context = model.send(:validation_context)
|
76
|
+
|
36
77
|
begin
|
37
78
|
model.send(:validation_context=, validation_context)
|
38
79
|
model.errors.clear
|
39
80
|
|
40
|
-
validate_callbacks = model._validate_callbacks.dup
|
41
|
-
validate_callbacks.each do |callback|
|
42
|
-
validate_callbacks.delete(callback) if callback.raw_filter.is_a? ActiveRecord::Validations::UniquenessValidator
|
43
|
-
end
|
44
|
-
|
45
81
|
model.run_callbacks(:validation) do
|
46
82
|
if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
|
47
|
-
runner = validate_callbacks.compile
|
83
|
+
runner = @validate_callbacks.compile
|
48
84
|
env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
|
49
85
|
if runner.respond_to?(:call) # ActiveRecord < 5.1
|
50
86
|
runner.call(env)
|
@@ -63,10 +99,10 @@ module ActiveRecord::Import #:nodoc:
|
|
63
99
|
runner.invoke_before(env)
|
64
100
|
runner.invoke_after(env)
|
65
101
|
end
|
66
|
-
elsif validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
67
|
-
model.instance_eval validate_callbacks.compile
|
102
|
+
elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
103
|
+
model.instance_eval @validate_callbacks.compile
|
68
104
|
else # ActiveRecord 3.x
|
69
|
-
model.instance_eval validate_callbacks.compile(nil, model)
|
105
|
+
model.instance_eval @validate_callbacks.compile(nil, model)
|
70
106
|
end
|
71
107
|
end
|
72
108
|
|
@@ -79,13 +115,14 @@ module ActiveRecord::Import #:nodoc:
|
|
79
115
|
end
|
80
116
|
|
81
117
|
class ActiveRecord::Associations::CollectionProxy
|
82
|
-
def
|
83
|
-
@association.
|
118
|
+
def bulk_import(*args, &block)
|
119
|
+
@association.bulk_import(*args, &block)
|
84
120
|
end
|
121
|
+
alias import bulk_import unless respond_to? :import
|
85
122
|
end
|
86
123
|
|
87
124
|
class ActiveRecord::Associations::CollectionAssociation
|
88
|
-
def
|
125
|
+
def bulk_import(*args, &block)
|
89
126
|
unless owner.persisted?
|
90
127
|
raise ActiveRecord::RecordNotSaved, "You cannot call import unless the parent is saved"
|
91
128
|
end
|
@@ -94,16 +131,21 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
94
131
|
|
95
132
|
model_klass = reflection.klass
|
96
133
|
symbolized_foreign_key = reflection.foreign_key.to_sym
|
97
|
-
symbolized_column_names = model_klass.column_names.map(&:to_sym)
|
98
134
|
|
99
|
-
|
135
|
+
symbolized_column_names = if model_klass.connection.respond_to?(:supports_virtual_columns?) && model_klass.connection.supports_virtual_columns?
|
136
|
+
model_klass.columns.reject(&:virtual?).map { |c| c.name.to_sym }
|
137
|
+
else
|
138
|
+
model_klass.column_names.map(&:to_sym)
|
139
|
+
end
|
140
|
+
|
141
|
+
owner_primary_key = reflection.active_record_primary_key.to_sym
|
100
142
|
owner_primary_key_value = owner.send(owner_primary_key)
|
101
143
|
|
102
144
|
# assume array of model objects
|
103
145
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
104
146
|
if args.length == 2
|
105
147
|
models = args.last
|
106
|
-
column_names = args.first
|
148
|
+
column_names = args.first.dup
|
107
149
|
else
|
108
150
|
models = args.first
|
109
151
|
column_names = symbolized_column_names
|
@@ -118,7 +160,46 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
118
160
|
m.public_send "#{reflection.type}=", owner.class.name if reflection.type
|
119
161
|
end
|
120
162
|
|
121
|
-
return model_klass.
|
163
|
+
return model_klass.bulk_import column_names, models, options
|
164
|
+
|
165
|
+
# supports array of hash objects
|
166
|
+
elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
|
167
|
+
if args.length == 2
|
168
|
+
array_of_hashes = args.last
|
169
|
+
column_names = args.first.dup
|
170
|
+
allow_extra_hash_keys = true
|
171
|
+
else
|
172
|
+
array_of_hashes = args.first
|
173
|
+
column_names = array_of_hashes.first.keys
|
174
|
+
allow_extra_hash_keys = false
|
175
|
+
end
|
176
|
+
|
177
|
+
symbolized_column_names = column_names.map(&:to_sym)
|
178
|
+
unless symbolized_column_names.include?(symbolized_foreign_key)
|
179
|
+
column_names << symbolized_foreign_key
|
180
|
+
end
|
181
|
+
|
182
|
+
if reflection.type && !symbolized_column_names.include?(reflection.type.to_sym)
|
183
|
+
column_names << reflection.type.to_sym
|
184
|
+
end
|
185
|
+
|
186
|
+
array_of_attributes = array_of_hashes.map do |h|
|
187
|
+
error_message = model_klass.send(:validate_hash_import, h, symbolized_column_names, allow_extra_hash_keys)
|
188
|
+
|
189
|
+
raise ArgumentError, error_message if error_message
|
190
|
+
|
191
|
+
column_names.map do |key|
|
192
|
+
if key == symbolized_foreign_key
|
193
|
+
owner_primary_key_value
|
194
|
+
elsif reflection.type && key == reflection.type.to_sym
|
195
|
+
owner.class.name
|
196
|
+
else
|
197
|
+
h[key]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
return model_klass.bulk_import column_names, array_of_attributes, options
|
122
203
|
|
123
204
|
# supports empty array
|
124
205
|
elsif args.last.is_a?( Array ) && args.last.empty?
|
@@ -127,7 +208,12 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
127
208
|
# supports 2-element array and array
|
128
209
|
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
129
210
|
column_names, array_of_attributes = args
|
130
|
-
|
211
|
+
|
212
|
+
# dup the passed args so we don't modify unintentionally
|
213
|
+
column_names = column_names.dup
|
214
|
+
array_of_attributes = array_of_attributes.map(&:dup)
|
215
|
+
|
216
|
+
symbolized_column_names = column_names.map(&:to_sym)
|
131
217
|
|
132
218
|
if symbolized_column_names.include?(symbolized_foreign_key)
|
133
219
|
index = symbolized_column_names.index(symbolized_foreign_key)
|
@@ -138,19 +224,35 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
138
224
|
end
|
139
225
|
|
140
226
|
if reflection.type
|
141
|
-
|
142
|
-
|
227
|
+
symbolized_type = reflection.type.to_sym
|
228
|
+
if symbolized_column_names.include?(symbolized_type)
|
229
|
+
index = symbolized_column_names.index(symbolized_type)
|
230
|
+
array_of_attributes.each { |attrs| attrs[index] = owner.class.name }
|
231
|
+
else
|
232
|
+
column_names << symbolized_type
|
233
|
+
array_of_attributes.each { |attrs| attrs << owner.class.name }
|
234
|
+
end
|
143
235
|
end
|
144
236
|
|
145
|
-
return model_klass.
|
237
|
+
return model_klass.bulk_import column_names, array_of_attributes, options
|
146
238
|
else
|
147
239
|
raise ArgumentError, "Invalid arguments!"
|
148
240
|
end
|
149
241
|
end
|
242
|
+
alias import bulk_import unless respond_to? :import
|
150
243
|
end
|
151
244
|
|
152
245
|
class ActiveRecord::Base
|
153
246
|
class << self
|
247
|
+
def establish_connection_with_activerecord_import(*args)
|
248
|
+
conn = establish_connection_without_activerecord_import(*args)
|
249
|
+
ActiveRecord::Import.load_from_connection_pool connection_pool
|
250
|
+
conn
|
251
|
+
end
|
252
|
+
|
253
|
+
alias establish_connection_without_activerecord_import establish_connection
|
254
|
+
alias establish_connection establish_connection_with_activerecord_import
|
255
|
+
|
154
256
|
# Returns true if the current database connection adapter
|
155
257
|
# supports import functionality, otherwise returns false.
|
156
258
|
def supports_import?(*args)
|
@@ -161,14 +263,14 @@ class ActiveRecord::Base
|
|
161
263
|
# supports on duplicate key update functionality, otherwise
|
162
264
|
# returns false.
|
163
265
|
def supports_on_duplicate_key_update?
|
164
|
-
connection.supports_on_duplicate_key_update?
|
266
|
+
connection.respond_to?(:supports_on_duplicate_key_update?) && connection.supports_on_duplicate_key_update?
|
165
267
|
end
|
166
268
|
|
167
269
|
# returns true if the current database connection adapter
|
168
270
|
# supports setting the primary key of bulk imported models, otherwise
|
169
271
|
# returns false
|
170
|
-
def
|
171
|
-
connection.respond_to?(:
|
272
|
+
def supports_setting_primary_key_of_imported_objects?
|
273
|
+
connection.respond_to?(:supports_setting_primary_key_of_imported_objects?) && connection.supports_setting_primary_key_of_imported_objects?
|
172
274
|
end
|
173
275
|
|
174
276
|
# Imports a collection of values to the database.
|
@@ -214,18 +316,21 @@ class ActiveRecord::Base
|
|
214
316
|
#
|
215
317
|
# == Options
|
216
318
|
# * +validate+ - true|false, tells import whether or not to use
|
217
|
-
#
|
319
|
+
# ActiveRecord validations. Validations are enforced by default.
|
320
|
+
# It skips the uniqueness validation for performance reasons.
|
321
|
+
# You can find more details here:
|
322
|
+
# https://github.com/zdennis/activerecord-import/issues/228
|
218
323
|
# * +ignore+ - true|false, an alias for on_duplicate_key_ignore.
|
219
324
|
# * +on_duplicate_key_ignore+ - true|false, tells import to discard
|
220
|
-
#
|
221
|
-
#
|
222
|
-
#
|
223
|
-
#
|
224
|
-
#
|
225
|
-
#
|
226
|
-
# * +on_duplicate_key_update+ - an Array or Hash, tells import to
|
227
|
-
#
|
228
|
-
#
|
325
|
+
# records that contain duplicate keys. For Postgres 9.5+ it adds
|
326
|
+
# ON CONFLICT DO NOTHING, for MySQL it uses INSERT IGNORE, and for
|
327
|
+
# SQLite it uses INSERT OR IGNORE. Cannot be enabled on a
|
328
|
+
# recursive import. For database adapters that normally support
|
329
|
+
# setting primary keys on imported objects, this option prevents
|
330
|
+
# that from occurring.
|
331
|
+
# * +on_duplicate_key_update+ - :all, an Array, or Hash, tells import to
|
332
|
+
# use MySQL's ON DUPLICATE KEY UPDATE or Postgres/SQLite ON CONFLICT
|
333
|
+
# DO UPDATE ability. See On Duplicate Key Update below.
|
229
334
|
# * +synchronize+ - an array of ActiveRecord instances for the model
|
230
335
|
# that you are currently importing data into. This synchronizes
|
231
336
|
# existing model instances in memory with updates from the import.
|
@@ -247,6 +352,9 @@ class ActiveRecord::Base
|
|
247
352
|
# BlogPost.import posts
|
248
353
|
#
|
249
354
|
# # Example using array_of_hash_objects
|
355
|
+
# # NOTE: column_names will be determined by using the keys of the first hash in the array. If later hashes in the
|
356
|
+
# # array have different keys an exception will be raised. If you have hashes to import with different sets of keys
|
357
|
+
# # we recommend grouping these into batches before importing.
|
250
358
|
# values = [ {author_name: 'zdennis', title: 'test post'} ], [ {author_name: 'jdoe', title: 'another test post'} ] ]
|
251
359
|
# BlogPost.import values
|
252
360
|
#
|
@@ -280,7 +388,15 @@ class ActiveRecord::Base
|
|
280
388
|
#
|
281
389
|
# == On Duplicate Key Update (MySQL)
|
282
390
|
#
|
283
|
-
# The :on_duplicate_key_update option can be either an Array or a Hash.
|
391
|
+
# The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
|
392
|
+
#
|
393
|
+
# ==== Using :all
|
394
|
+
#
|
395
|
+
# The :on_duplicate_key_update option can be set to :all. All columns
|
396
|
+
# other than the primary key are updated. If a list of column names is
|
397
|
+
# supplied, only those columns will be updated. Below is an example:
|
398
|
+
#
|
399
|
+
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
284
400
|
#
|
285
401
|
# ==== Using an Array
|
286
402
|
#
|
@@ -299,11 +415,19 @@ class ActiveRecord::Base
|
|
299
415
|
#
|
300
416
|
# BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
|
301
417
|
#
|
302
|
-
# == On Duplicate Key Update (Postgres 9.5+)
|
418
|
+
# == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
|
303
419
|
#
|
304
|
-
# The :on_duplicate_key_update option can be an Array or a Hash with up to
|
420
|
+
# The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
|
305
421
|
# three attributes, :conflict_target (and optionally :index_predicate) or
|
306
|
-
# :constraint_name, and :columns.
|
422
|
+
# :constraint_name (Postgres), and :columns.
|
423
|
+
#
|
424
|
+
# ==== Using :all
|
425
|
+
#
|
426
|
+
# The :on_duplicate_key_update option can be set to :all. All columns
|
427
|
+
# other than the primary key are updated. If a list of column names is
|
428
|
+
# supplied, only those columns will be updated. Below is an example:
|
429
|
+
#
|
430
|
+
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
307
431
|
#
|
308
432
|
# ==== Using an Array
|
309
433
|
#
|
@@ -323,7 +447,7 @@ class ActiveRecord::Base
|
|
323
447
|
# conflicting constraint to be explicitly specified. Using this option
|
324
448
|
# allows you to specify a constraint other than the primary key.
|
325
449
|
#
|
326
|
-
#
|
450
|
+
# ===== :conflict_target
|
327
451
|
#
|
328
452
|
# The :conflict_target attribute specifies the columns that make up the
|
329
453
|
# conflicting unique constraint and can be a single column or an array of
|
@@ -333,7 +457,7 @@ class ActiveRecord::Base
|
|
333
457
|
#
|
334
458
|
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], columns: [ :date_modified ] }
|
335
459
|
#
|
336
|
-
#
|
460
|
+
# ===== :index_predicate
|
337
461
|
#
|
338
462
|
# The :index_predicate attribute optionally specifies a WHERE condition
|
339
463
|
# on :conflict_target, which is required for matching against partial
|
@@ -342,7 +466,7 @@ class ActiveRecord::Base
|
|
342
466
|
#
|
343
467
|
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], index_predicate: 'status <> 0', columns: [ :date_modified ] }
|
344
468
|
#
|
345
|
-
#
|
469
|
+
# ===== :constraint_name
|
346
470
|
#
|
347
471
|
# The :constraint_name attribute explicitly identifies the conflicting
|
348
472
|
# unique index by name. Postgres documentation discourages using this method
|
@@ -350,7 +474,7 @@ class ActiveRecord::Base
|
|
350
474
|
#
|
351
475
|
# BlogPost.import columns, values, on_duplicate_key_update: { constraint_name: :blog_posts_pkey, columns: [ :date_modified ] }
|
352
476
|
#
|
353
|
-
#
|
477
|
+
# ===== :condition
|
354
478
|
#
|
355
479
|
# The :condition attribute optionally specifies a WHERE condition
|
356
480
|
# on :conflict_action. Only rows for which this expression returns true will be updated.
|
@@ -358,12 +482,20 @@ class ActiveRecord::Base
|
|
358
482
|
# Below is an example:
|
359
483
|
#
|
360
484
|
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id ], condition: "blog_posts.title NOT LIKE '%sample%'", columns: [ :author_name ] }
|
361
|
-
|
362
|
-
# ====== :columns
|
363
485
|
#
|
364
|
-
#
|
486
|
+
# ===== :columns
|
487
|
+
#
|
488
|
+
# The :columns attribute can be either :all, an Array, or a Hash.
|
489
|
+
#
|
490
|
+
# ===== Using :all
|
491
|
+
#
|
492
|
+
# The :columns attribute can be :all. All columns other than the primary key will be updated.
|
493
|
+
# If a list of column names is supplied, only those columns will be updated.
|
494
|
+
# Below is an example:
|
495
|
+
#
|
496
|
+
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
|
365
497
|
#
|
366
|
-
#
|
498
|
+
# ===== Using an Array
|
367
499
|
#
|
368
500
|
# The :columns attribute can be an array of column names. The column names
|
369
501
|
# are the only fields that are updated if a duplicate record is found.
|
@@ -371,7 +503,7 @@ class ActiveRecord::Base
|
|
371
503
|
#
|
372
504
|
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: [ :date_modified, :content, :author ] }
|
373
505
|
#
|
374
|
-
#
|
506
|
+
# ===== Using a Hash
|
375
507
|
#
|
376
508
|
# The :columns option can be a hash of column names to model attribute name
|
377
509
|
# mappings. This gives you finer grained control over what fields are updated
|
@@ -384,7 +516,8 @@ class ActiveRecord::Base
|
|
384
516
|
# * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
|
385
517
|
# * num_inserts - the number of insert statements it took to import the data
|
386
518
|
# * ids - the primary keys of the imported ids if the adapter supports it, otherwise an empty array.
|
387
|
-
|
519
|
+
# * results - import results if the adapter supports it, otherwise an empty array.
|
520
|
+
def bulk_import(*args)
|
388
521
|
if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
|
389
522
|
options = {}
|
390
523
|
options.merge!( args.pop ) if args.last.is_a?(Hash)
|
@@ -395,31 +528,29 @@ class ActiveRecord::Base
|
|
395
528
|
import_helper(*args)
|
396
529
|
end
|
397
530
|
end
|
531
|
+
alias import bulk_import unless respond_to? :import
|
398
532
|
|
399
533
|
# Imports a collection of values if all values are valid. Import fails at the
|
400
534
|
# first encountered validation error and raises ActiveRecord::RecordInvalid
|
401
535
|
# with the failed instance.
|
402
|
-
def
|
536
|
+
def bulk_import!(*args)
|
403
537
|
options = args.last.is_a?( Hash ) ? args.pop : {}
|
404
538
|
options[:validate] = true
|
405
539
|
options[:raise_error] = true
|
406
540
|
|
407
|
-
|
541
|
+
bulk_import(*args, options)
|
408
542
|
end
|
543
|
+
alias import! bulk_import! unless respond_to? :import!
|
409
544
|
|
410
545
|
def import_helper( *args )
|
411
546
|
options = { validate: true, timestamps: true }
|
412
547
|
options.merge!( args.pop ) if args.last.is_a? Hash
|
413
548
|
# making sure that current model's primary key is used
|
414
549
|
options[:primary_key] = primary_key
|
550
|
+
options[:locking_column] = locking_column if attribute_names.include?(locking_column)
|
415
551
|
|
416
|
-
|
417
|
-
|
418
|
-
options[:on_duplicate_key_update] = options[:on_duplicate_key_update].dup
|
419
|
-
end
|
420
|
-
|
421
|
-
is_validating = options[:validate]
|
422
|
-
is_validating = true unless options[:validate_with_context].nil?
|
552
|
+
is_validating = options[:validate_with_context].present? ? true : options[:validate]
|
553
|
+
validator = ActiveRecord::Import::Validator.new(self, options)
|
423
554
|
|
424
555
|
# assume array of model objects
|
425
556
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
@@ -428,11 +559,19 @@ class ActiveRecord::Base
|
|
428
559
|
column_names = args.first.dup
|
429
560
|
else
|
430
561
|
models = args.first
|
431
|
-
column_names =
|
562
|
+
column_names = if connection.respond_to?(:supports_virtual_columns?) && connection.supports_virtual_columns?
|
563
|
+
columns.reject(&:virtual?).map(&:name)
|
564
|
+
else
|
565
|
+
self.column_names.dup
|
566
|
+
end
|
432
567
|
end
|
433
568
|
|
434
|
-
if models.first.id.nil?
|
435
|
-
|
569
|
+
if models.first.id.nil?
|
570
|
+
Array(primary_key).each do |c|
|
571
|
+
if column_names.include?(c) && columns_hash[c].type == :uuid
|
572
|
+
column_names.delete(c)
|
573
|
+
end
|
574
|
+
end
|
436
575
|
end
|
437
576
|
|
438
577
|
default_values = column_defaults
|
@@ -444,11 +583,34 @@ class ActiveRecord::Base
|
|
444
583
|
serialized_attributes
|
445
584
|
end
|
446
585
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
586
|
+
update_attrs = if record_timestamps && options[:timestamps]
|
587
|
+
if respond_to?(:timestamp_attributes_for_update, true)
|
588
|
+
send(:timestamp_attributes_for_update).map(&:to_sym)
|
589
|
+
else
|
590
|
+
new.send(:timestamp_attributes_for_update_in_model)
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
array_of_attributes = []
|
595
|
+
|
596
|
+
models.each do |model|
|
597
|
+
if supports_setting_primary_key_of_imported_objects?
|
598
|
+
load_association_ids(model)
|
599
|
+
end
|
600
|
+
|
601
|
+
if is_validating && !validator.valid_model?(model)
|
602
|
+
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
603
|
+
next
|
604
|
+
end
|
605
|
+
|
606
|
+
array_of_attributes << column_names.map do |name|
|
607
|
+
if model.persisted? &&
|
608
|
+
update_attrs && update_attrs.include?(name.to_sym) &&
|
609
|
+
!model.send("#{name}_changed?")
|
610
|
+
nil
|
611
|
+
elsif stored_attrs.key?(name.to_sym) ||
|
612
|
+
serialized_attrs.key?(name.to_s) ||
|
613
|
+
default_values[name.to_s]
|
452
614
|
model.read_attribute(name.to_s)
|
453
615
|
else
|
454
616
|
model.read_attribute_before_type_cast(name.to_s)
|
@@ -460,12 +622,18 @@ class ActiveRecord::Base
|
|
460
622
|
if args.length == 2
|
461
623
|
array_of_hashes = args.last
|
462
624
|
column_names = args.first.dup
|
625
|
+
allow_extra_hash_keys = true
|
463
626
|
else
|
464
627
|
array_of_hashes = args.first
|
465
628
|
column_names = array_of_hashes.first.keys
|
629
|
+
allow_extra_hash_keys = false
|
466
630
|
end
|
467
631
|
|
468
632
|
array_of_attributes = array_of_hashes.map do |h|
|
633
|
+
error_message = validate_hash_import(h, column_names, allow_extra_hash_keys)
|
634
|
+
|
635
|
+
raise ArgumentError, error_message if error_message
|
636
|
+
|
469
637
|
column_names.map do |key|
|
470
638
|
h[key]
|
471
639
|
end
|
@@ -497,47 +665,78 @@ class ActiveRecord::Base
|
|
497
665
|
|
498
666
|
if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && connection.prefetch_primary_key? && sequence_name
|
499
667
|
column_count = column_names.size
|
500
|
-
column_names.concat(primary_key).uniq!
|
668
|
+
column_names.concat(Array(primary_key)).uniq!
|
501
669
|
columns_added = column_names.size - column_count
|
502
670
|
new_fields = Array.new(columns_added)
|
503
671
|
array_of_attributes.each { |a| a.concat(new_fields) }
|
504
672
|
end
|
505
673
|
|
674
|
+
# Don't modify incoming arguments
|
675
|
+
on_duplicate_key_update = options[:on_duplicate_key_update]
|
676
|
+
if on_duplicate_key_update
|
677
|
+
updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
|
678
|
+
options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
|
679
|
+
on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
|
680
|
+
duped_options[k] = if k == :columns && v == :all
|
681
|
+
updatable_columns
|
682
|
+
elsif v.duplicable?
|
683
|
+
v.dup
|
684
|
+
else
|
685
|
+
v
|
686
|
+
end
|
687
|
+
end
|
688
|
+
elsif on_duplicate_key_update == :all
|
689
|
+
updatable_columns
|
690
|
+
elsif on_duplicate_key_update.duplicable?
|
691
|
+
on_duplicate_key_update.dup
|
692
|
+
else
|
693
|
+
on_duplicate_key_update
|
694
|
+
end
|
695
|
+
end
|
696
|
+
|
506
697
|
timestamps = {}
|
507
698
|
|
508
699
|
# record timestamps unless disabled in ActiveRecord::Base
|
509
|
-
if record_timestamps && options
|
700
|
+
if record_timestamps && options[:timestamps]
|
510
701
|
timestamps = add_special_rails_stamps column_names, array_of_attributes, options
|
511
702
|
end
|
512
703
|
|
513
704
|
return_obj = if is_validating
|
514
|
-
|
515
|
-
|
516
|
-
models.
|
517
|
-
|
518
|
-
|
705
|
+
import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
|
706
|
+
if models
|
707
|
+
models.each { |m| failed_instances << m if m.errors.any? }
|
708
|
+
else
|
709
|
+
# create instances for each of our column/value sets
|
710
|
+
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
711
|
+
|
712
|
+
# keep track of the instance and the position it is currently at. if this fails
|
713
|
+
# validation we'll use the index to remove it from the array_of_attributes
|
714
|
+
arr.each_with_index do |hsh, i|
|
715
|
+
model = new
|
716
|
+
hsh.each_pair { |k, v| model[k] = v }
|
717
|
+
next if validator.valid_model?(model)
|
519
718
|
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
520
719
|
array_of_attributes[i] = nil
|
521
|
-
|
720
|
+
failure = model.dup
|
721
|
+
failure.errors.send(:initialize_dup, model.errors)
|
722
|
+
failed_instances << failure
|
522
723
|
end
|
724
|
+
array_of_attributes.compact!
|
523
725
|
end
|
524
|
-
else
|
525
|
-
import_with_validations( column_names, array_of_attributes, options )
|
526
726
|
end
|
527
727
|
else
|
528
|
-
|
529
|
-
ActiveRecord::Import::Result.new([], num_inserts, ids)
|
728
|
+
import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
530
729
|
end
|
531
730
|
|
532
731
|
if options[:synchronize]
|
533
|
-
sync_keys = options[:synchronize_keys] ||
|
732
|
+
sync_keys = options[:synchronize_keys] || Array(primary_key)
|
534
733
|
synchronize( options[:synchronize], sync_keys)
|
535
734
|
end
|
536
735
|
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
537
736
|
|
538
737
|
# if we have ids, then set the id on the models and mark the models as clean.
|
539
|
-
if models &&
|
540
|
-
set_attributes_and_mark_clean(models, return_obj, timestamps)
|
738
|
+
if models && supports_setting_primary_key_of_imported_objects?
|
739
|
+
set_attributes_and_mark_clean(models, return_obj, timestamps, options)
|
541
740
|
|
542
741
|
# if there are auto-save associations on the models we imported that are new, import them as well
|
543
742
|
import_associations(models, options.dup) if options[:recursive]
|
@@ -556,36 +755,14 @@ class ActiveRecord::Base
|
|
556
755
|
def import_with_validations( column_names, array_of_attributes, options = {} )
|
557
756
|
failed_instances = []
|
558
757
|
|
559
|
-
|
560
|
-
|
561
|
-
if block_given?
|
562
|
-
yield validator, failed_instances
|
563
|
-
else
|
564
|
-
# create instances for each of our column/value sets
|
565
|
-
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
566
|
-
|
567
|
-
# keep track of the instance and the position it is currently at. if this fails
|
568
|
-
# validation we'll use the index to remove it from the array_of_attributes
|
569
|
-
model = new
|
570
|
-
arr.each_with_index do |hsh, i|
|
571
|
-
hsh.each_pair { |k, v| model[k] = v }
|
572
|
-
next if validator.valid_model? model
|
573
|
-
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
574
|
-
array_of_attributes[i] = nil
|
575
|
-
failure = model.dup
|
576
|
-
failure.errors.send(:initialize_dup, model.errors)
|
577
|
-
failed_instances << failure
|
578
|
-
end
|
579
|
-
end
|
580
|
-
|
581
|
-
array_of_attributes.compact!
|
758
|
+
yield failed_instances if block_given?
|
582
759
|
|
583
|
-
|
584
|
-
[0, []]
|
760
|
+
result = if options[:all_or_none] && failed_instances.any?
|
761
|
+
ActiveRecord::Import::Result.new([], 0, [], [])
|
585
762
|
else
|
586
763
|
import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
587
764
|
end
|
588
|
-
ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids)
|
765
|
+
ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results)
|
589
766
|
end
|
590
767
|
|
591
768
|
# Imports the passed in +column_names+ and +array_of_attributes+
|
@@ -595,6 +772,8 @@ class ActiveRecord::Base
|
|
595
772
|
# information on +column_names+, +array_of_attributes_ and
|
596
773
|
# +options+.
|
597
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
|
+
|
598
777
|
column_names = column_names.map(&:to_sym)
|
599
778
|
scope_columns, scope_values = scope_attributes.to_a.transpose
|
600
779
|
|
@@ -604,7 +783,7 @@ class ActiveRecord::Base
|
|
604
783
|
next if column_names.include?(name_as_sym)
|
605
784
|
|
606
785
|
is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
|
607
|
-
value = value.first if is_sti
|
786
|
+
value = Array(value).first if is_sti
|
608
787
|
|
609
788
|
column_names << name_as_sym
|
610
789
|
array_of_attributes.each { |attrs| attrs << value }
|
@@ -627,6 +806,7 @@ class ActiveRecord::Base
|
|
627
806
|
|
628
807
|
number_inserted = 0
|
629
808
|
ids = []
|
809
|
+
results = []
|
630
810
|
if supports_import?
|
631
811
|
# generate the sql
|
632
812
|
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
@@ -637,9 +817,10 @@ class ActiveRecord::Base
|
|
637
817
|
result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
|
638
818
|
batch_values,
|
639
819
|
options,
|
640
|
-
"#{
|
641
|
-
number_inserted += result
|
642
|
-
ids += result
|
820
|
+
"#{model_name} Create Many Without Validations Or Callbacks" )
|
821
|
+
number_inserted += result.num_inserts
|
822
|
+
ids += result.ids
|
823
|
+
results += result.results
|
643
824
|
end
|
644
825
|
else
|
645
826
|
transaction(requires_new: true) do
|
@@ -649,22 +830,14 @@ class ActiveRecord::Base
|
|
649
830
|
end
|
650
831
|
end
|
651
832
|
end
|
652
|
-
[number_inserted, ids
|
833
|
+
ActiveRecord::Import::Result.new([], number_inserted, ids, results)
|
653
834
|
end
|
654
835
|
|
655
836
|
private
|
656
837
|
|
657
|
-
def set_attributes_and_mark_clean(models, import_result, timestamps)
|
838
|
+
def set_attributes_and_mark_clean(models, import_result, timestamps, options)
|
658
839
|
return if models.nil?
|
659
840
|
models -= import_result.failed_instances
|
660
|
-
models.each do |model|
|
661
|
-
if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
|
662
|
-
model.clear_changes_information
|
663
|
-
else # Rails 3.2
|
664
|
-
model.instance_variable_get(:@changed_attributes).clear
|
665
|
-
end
|
666
|
-
model.instance_variable_set(:@new_record, false)
|
667
|
-
end
|
668
841
|
|
669
842
|
# if ids were returned for all models we know all were updated
|
670
843
|
if models.size == import_result.ids.size
|
@@ -677,6 +850,49 @@ class ActiveRecord::Base
|
|
677
850
|
end
|
678
851
|
end
|
679
852
|
end
|
853
|
+
|
854
|
+
if models.size == import_result.results.size
|
855
|
+
columns = Array(options[:returning])
|
856
|
+
single_column = "#{columns.first}=" if columns.size == 1
|
857
|
+
import_result.results.each_with_index do |result, index|
|
858
|
+
model = models[index]
|
859
|
+
|
860
|
+
if single_column
|
861
|
+
model.send(single_column, result)
|
862
|
+
else
|
863
|
+
columns.each_with_index do |column, col_index|
|
864
|
+
model.send("#{column}=", result[col_index])
|
865
|
+
end
|
866
|
+
end
|
867
|
+
end
|
868
|
+
end
|
869
|
+
|
870
|
+
models.each do |model|
|
871
|
+
if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
|
872
|
+
model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
|
873
|
+
model.changes_applied
|
874
|
+
elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
|
875
|
+
model.clear_changes_information
|
876
|
+
else # Rails 3.2
|
877
|
+
model.instance_variable_get(:@changed_attributes).clear
|
878
|
+
end
|
879
|
+
model.instance_variable_set(:@new_record, false)
|
880
|
+
end
|
881
|
+
end
|
882
|
+
|
883
|
+
# Sync belongs_to association ids with foreign key field
|
884
|
+
def load_association_ids(model)
|
885
|
+
association_reflections = model.class.reflect_on_all_associations(:belongs_to)
|
886
|
+
association_reflections.each do |association_reflection|
|
887
|
+
column_name = association_reflection.foreign_key
|
888
|
+
next if association_reflection.options[:polymorphic]
|
889
|
+
association = model.association(association_reflection.name)
|
890
|
+
association = association.target
|
891
|
+
next if association.blank? || model.public_send(column_name).present?
|
892
|
+
|
893
|
+
association_primary_key = association_reflection.association_primary_key
|
894
|
+
model.public_send("#{column_name}=", association.send(association_primary_key))
|
895
|
+
end
|
680
896
|
end
|
681
897
|
|
682
898
|
def import_associations(models, options)
|
@@ -693,7 +909,7 @@ class ActiveRecord::Base
|
|
693
909
|
|
694
910
|
associated_objects_by_class.each_value do |associations|
|
695
911
|
associations.each_value do |associated_records|
|
696
|
-
associated_records.first.class.
|
912
|
+
associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
|
697
913
|
end
|
698
914
|
end
|
699
915
|
end
|
@@ -702,6 +918,7 @@ class ActiveRecord::Base
|
|
702
918
|
# of class => objects to import.
|
703
919
|
def find_associated_objects_for_import(associated_objects_by_class, model)
|
704
920
|
associated_objects_by_class[model.class.name] ||= {}
|
921
|
+
return associated_objects_by_class unless model.id
|
705
922
|
|
706
923
|
association_reflections =
|
707
924
|
model.class.reflect_on_all_associations(:has_one) +
|
@@ -740,8 +957,10 @@ class ActiveRecord::Base
|
|
740
957
|
column = columns[j]
|
741
958
|
|
742
959
|
# be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
|
743
|
-
if val.nil? &&
|
960
|
+
if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
|
744
961
|
connection_memo.next_value_for_sequence(sequence_name)
|
962
|
+
elsif val.respond_to?(:to_sql)
|
963
|
+
"(#{val.to_sql})"
|
745
964
|
elsif column
|
746
965
|
if respond_to?(:type_caster) # Rails 5.0 and higher
|
747
966
|
type = type_for_attribute(column.name)
|
@@ -753,7 +972,9 @@ class ActiveRecord::Base
|
|
753
972
|
if serialized_attributes.include?(column.name)
|
754
973
|
val = serialized_attributes[column.name].dump(val)
|
755
974
|
end
|
756
|
-
|
975
|
+
# Fixes #443 to support binary (i.e. bytea) columns on PG
|
976
|
+
val = column.type_cast(val) unless column.type.to_sym == :binary
|
977
|
+
connection_memo.quote(val, column)
|
757
978
|
end
|
758
979
|
end
|
759
980
|
end
|
@@ -785,7 +1006,7 @@ class ActiveRecord::Base
|
|
785
1006
|
index = column_names.index(column) || column_names.index(column.to_sym)
|
786
1007
|
if index
|
787
1008
|
# replace every instance of the array of attributes with our value
|
788
|
-
array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil?
|
1009
|
+
array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
|
789
1010
|
else
|
790
1011
|
column_names << column
|
791
1012
|
array_of_attributes.each { |arr| arr << timestamp }
|
@@ -804,5 +1025,42 @@ class ActiveRecord::Base
|
|
804
1025
|
def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
|
805
1026
|
array_of_attributes.map { |values| Hash[column_names.zip(values)] }
|
806
1027
|
end
|
1028
|
+
|
1029
|
+
# Checks that the imported hash has the required_keys, optionally also checks that the hash has
|
1030
|
+
# no keys beyond those required when `allow_extra_keys` is false.
|
1031
|
+
# returns `nil` if validation passes, or an error message if it fails
|
1032
|
+
def validate_hash_import(hash, required_keys, allow_extra_keys) # :nodoc:
|
1033
|
+
extra_keys = allow_extra_keys ? [] : hash.keys - required_keys
|
1034
|
+
missing_keys = required_keys - hash.keys
|
1035
|
+
|
1036
|
+
return nil if extra_keys.empty? && missing_keys.empty?
|
1037
|
+
|
1038
|
+
if allow_extra_keys
|
1039
|
+
<<-EOS
|
1040
|
+
Hash key mismatch.
|
1041
|
+
|
1042
|
+
When importing an array of hashes with provided columns_names, each hash must contain keys for all column_names.
|
1043
|
+
|
1044
|
+
Required keys: #{required_keys}
|
1045
|
+
Missing keys: #{missing_keys}
|
1046
|
+
|
1047
|
+
Hash: #{hash}
|
1048
|
+
EOS
|
1049
|
+
else
|
1050
|
+
<<-EOS
|
1051
|
+
Hash key mismatch.
|
1052
|
+
|
1053
|
+
When importing an array of hashes, all hashes must have the same keys.
|
1054
|
+
If you have records that are missing some values, we recommend you either set default values
|
1055
|
+
for the missing keys or group these records into batches by key set before importing.
|
1056
|
+
|
1057
|
+
Required keys: #{required_keys}
|
1058
|
+
Extra keys: #{extra_keys}
|
1059
|
+
Missing keys: #{missing_keys}
|
1060
|
+
|
1061
|
+
Hash: #{hash}
|
1062
|
+
EOS
|
1063
|
+
end
|
1064
|
+
end
|
807
1065
|
end
|
808
1066
|
end
|