activerecord-import 0.17.2 → 1.1.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/.gitignore +1 -0
- data/.travis.yml +40 -23
- data/CHANGELOG.md +315 -1
- data/Gemfile +23 -13
- data/LICENSE +21 -56
- data/README.markdown +564 -33
- data/Rakefile +2 -1
- data/activerecord-import.gemspec +3 -3
- data/benchmarks/lib/cli_parser.rb +2 -1
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
- data/gemfiles/5.1.gemfile +2 -0
- data/gemfiles/5.2.gemfile +2 -0
- data/gemfiles/6.0.gemfile +2 -0
- data/gemfiles/6.1.gemfile +1 -0
- data/lib/activerecord-import.rb +2 -15
- data/lib/activerecord-import/adapters/abstract_adapter.rb +9 -3
- data/lib/activerecord-import/adapters/mysql_adapter.rb +17 -11
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +68 -20
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
- data/lib/activerecord-import/base.rb +12 -7
- data/lib/activerecord-import/import.rb +514 -166
- data/lib/activerecord-import/synchronize.rb +2 -2
- data/lib/activerecord-import/value_sets_parser.rb +16 -0
- data/lib/activerecord-import/version.rb +1 -1
- data/test/adapters/makara_postgis.rb +1 -0
- data/test/import_test.rb +274 -23
- data/test/makara_postgis/import_test.rb +8 -0
- data/test/models/account.rb +3 -0
- data/test/models/animal.rb +6 -0
- data/test/models/bike_maker.rb +7 -0
- data/test/models/tag.rb +1 -1
- data/test/models/topic.rb +14 -0
- data/test/models/user.rb +3 -0
- data/test/models/user_token.rb +4 -0
- data/test/schema/generic_schema.rb +30 -8
- data/test/schema/mysql2_schema.rb +19 -0
- data/test/schema/postgresql_schema.rb +18 -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 +220 -1
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +15 -9
- data/test/support/shared_examples/on_duplicate_key_update.rb +271 -8
- data/test/support/shared_examples/recursive_import.rb +91 -21
- data/test/support/sqlite3/import_examples.rb +189 -25
- data/test/synchronize_test.rb +8 -0
- data/test/test_helper.rb +24 -3
- data/test/value_sets_bytes_parser_test.rb +13 -2
- metadata +32 -13
- data/test/schema/mysql_schema.rb +0 -16
@@ -11,24 +11,29 @@ module ActiveRecord::Import
|
|
11
11
|
when 'mysql2spatial' then 'mysql2'
|
12
12
|
when 'spatialite' then 'sqlite3'
|
13
13
|
when 'postgresql_makara' then 'postgresql'
|
14
|
+
when 'makara_postgis' then 'postgresql'
|
14
15
|
when 'postgis' then 'postgresql'
|
16
|
+
when 'cockroachdb' then 'postgresql'
|
15
17
|
else adapter
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
19
21
|
# Loads the import functionality for a specific database adapter
|
20
22
|
def self.require_adapter(adapter)
|
21
|
-
require File.join(ADAPTER_PATH, "
|
22
|
-
|
23
|
-
|
24
|
-
rescue LoadError
|
25
|
-
# fallback
|
26
|
-
end
|
23
|
+
require File.join(ADAPTER_PATH, "/#{base_adapter(adapter)}_adapter")
|
24
|
+
rescue LoadError
|
25
|
+
# fallback
|
27
26
|
end
|
28
27
|
|
29
28
|
# Loads the import functionality for the passed in ActiveRecord connection
|
30
29
|
def self.load_from_connection_pool(connection_pool)
|
31
|
-
|
30
|
+
adapter =
|
31
|
+
if connection_pool.respond_to?(:db_config) # ActiveRecord >= 6.1
|
32
|
+
connection_pool.db_config.adapter
|
33
|
+
else
|
34
|
+
connection_pool.spec.config[:adapter]
|
35
|
+
end
|
36
|
+
require_adapter adapter
|
32
37
|
end
|
33
38
|
end
|
34
39
|
|
@@ -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:
|
@@ -22,16 +22,110 @@ module ActiveRecord::Import #:nodoc:
|
|
22
22
|
super "Missing column for value <#{name}> at index #{index}"
|
23
23
|
end
|
24
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
|
25
118
|
end
|
26
119
|
|
27
120
|
class ActiveRecord::Associations::CollectionProxy
|
28
|
-
def
|
29
|
-
@association.
|
121
|
+
def bulk_import(*args, &block)
|
122
|
+
@association.bulk_import(*args, &block)
|
30
123
|
end
|
124
|
+
alias import bulk_import unless respond_to? :import
|
31
125
|
end
|
32
126
|
|
33
127
|
class ActiveRecord::Associations::CollectionAssociation
|
34
|
-
def
|
128
|
+
def bulk_import(*args, &block)
|
35
129
|
unless owner.persisted?
|
36
130
|
raise ActiveRecord::RecordNotSaved, "You cannot call import unless the parent is saved"
|
37
131
|
end
|
@@ -40,16 +134,21 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
40
134
|
|
41
135
|
model_klass = reflection.klass
|
42
136
|
symbolized_foreign_key = reflection.foreign_key.to_sym
|
43
|
-
symbolized_column_names = model_klass.column_names.map(&:to_sym)
|
44
137
|
|
45
|
-
|
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
|
46
145
|
owner_primary_key_value = owner.send(owner_primary_key)
|
47
146
|
|
48
147
|
# assume array of model objects
|
49
148
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
50
149
|
if args.length == 2
|
51
150
|
models = args.last
|
52
|
-
column_names = args.first
|
151
|
+
column_names = args.first.dup
|
53
152
|
else
|
54
153
|
models = args.first
|
55
154
|
column_names = symbolized_column_names
|
@@ -64,7 +163,46 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
64
163
|
m.public_send "#{reflection.type}=", owner.class.name if reflection.type
|
65
164
|
end
|
66
165
|
|
67
|
-
return model_klass.
|
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
|
187
|
+
end
|
188
|
+
|
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
208
|
elsif args.last.is_a?( Array ) && args.last.empty?
|
@@ -73,7 +211,12 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
73
211
|
# supports 2-element array and array
|
74
212
|
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
75
213
|
column_names, array_of_attributes = args
|
76
|
-
|
214
|
+
|
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)
|
77
220
|
|
78
221
|
if symbolized_column_names.include?(symbolized_foreign_key)
|
79
222
|
index = symbolized_column_names.index(symbolized_foreign_key)
|
@@ -84,31 +227,35 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
84
227
|
end
|
85
228
|
|
86
229
|
if reflection.type
|
87
|
-
|
88
|
-
|
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
|
89
238
|
end
|
90
239
|
|
91
|
-
return model_klass.
|
240
|
+
return model_klass.bulk_import column_names, array_of_attributes, options
|
92
241
|
else
|
93
242
|
raise ArgumentError, "Invalid arguments!"
|
94
243
|
end
|
95
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
|
96
254
|
end
|
97
255
|
|
98
256
|
class ActiveRecord::Base
|
99
257
|
class << self
|
100
|
-
|
101
|
-
tproc = lambda do
|
102
|
-
ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
|
103
|
-
end
|
104
|
-
|
105
|
-
AREXT_RAILS_COLUMNS = {
|
106
|
-
create: { "created_on" => tproc,
|
107
|
-
"created_at" => tproc },
|
108
|
-
update: { "updated_on" => tproc,
|
109
|
-
"updated_at" => tproc }
|
110
|
-
}.freeze
|
111
|
-
AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
|
258
|
+
prepend ActiveRecord::Import::Connection
|
112
259
|
|
113
260
|
# Returns true if the current database connection adapter
|
114
261
|
# supports import functionality, otherwise returns false.
|
@@ -120,14 +267,14 @@ class ActiveRecord::Base
|
|
120
267
|
# supports on duplicate key update functionality, otherwise
|
121
268
|
# returns false.
|
122
269
|
def supports_on_duplicate_key_update?
|
123
|
-
connection.supports_on_duplicate_key_update?
|
270
|
+
connection.respond_to?(:supports_on_duplicate_key_update?) && connection.supports_on_duplicate_key_update?
|
124
271
|
end
|
125
272
|
|
126
273
|
# returns true if the current database connection adapter
|
127
274
|
# supports setting the primary key of bulk imported models, otherwise
|
128
275
|
# returns false
|
129
|
-
def
|
130
|
-
connection.respond_to?(:
|
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?
|
131
278
|
end
|
132
279
|
|
133
280
|
# Imports a collection of values to the database.
|
@@ -173,16 +320,21 @@ class ActiveRecord::Base
|
|
173
320
|
#
|
174
321
|
# == Options
|
175
322
|
# * +validate+ - true|false, tells import whether or not to use
|
176
|
-
#
|
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
|
177
327
|
# * +ignore+ - true|false, an alias for on_duplicate_key_ignore.
|
178
328
|
# * +on_duplicate_key_ignore+ - true|false, tells import to discard
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
184
|
-
#
|
185
|
-
#
|
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.
|
186
338
|
# * +synchronize+ - an array of ActiveRecord instances for the model
|
187
339
|
# that you are currently importing data into. This synchronizes
|
188
340
|
# existing model instances in memory with updates from the import.
|
@@ -204,6 +356,9 @@ class ActiveRecord::Base
|
|
204
356
|
# BlogPost.import posts
|
205
357
|
#
|
206
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.
|
207
362
|
# values = [ {author_name: 'zdennis', title: 'test post'} ], [ {author_name: 'jdoe', title: 'another test post'} ] ]
|
208
363
|
# BlogPost.import values
|
209
364
|
#
|
@@ -237,7 +392,15 @@ class ActiveRecord::Base
|
|
237
392
|
#
|
238
393
|
# == On Duplicate Key Update (MySQL)
|
239
394
|
#
|
240
|
-
# The :on_duplicate_key_update option can be either an Array or a Hash.
|
395
|
+
# The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
|
396
|
+
#
|
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
|
241
404
|
#
|
242
405
|
# ==== Using an Array
|
243
406
|
#
|
@@ -256,11 +419,19 @@ class ActiveRecord::Base
|
|
256
419
|
#
|
257
420
|
# BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
|
258
421
|
#
|
259
|
-
# == On Duplicate Key Update (Postgres 9.5+)
|
422
|
+
# == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
|
260
423
|
#
|
261
|
-
# The :on_duplicate_key_update option can be an Array or a Hash with up to
|
424
|
+
# The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
|
262
425
|
# three attributes, :conflict_target (and optionally :index_predicate) or
|
263
|
-
# :constraint_name, and :columns.
|
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
|
264
435
|
#
|
265
436
|
# ==== Using an Array
|
266
437
|
#
|
@@ -280,7 +451,7 @@ class ActiveRecord::Base
|
|
280
451
|
# conflicting constraint to be explicitly specified. Using this option
|
281
452
|
# allows you to specify a constraint other than the primary key.
|
282
453
|
#
|
283
|
-
#
|
454
|
+
# ===== :conflict_target
|
284
455
|
#
|
285
456
|
# The :conflict_target attribute specifies the columns that make up the
|
286
457
|
# conflicting unique constraint and can be a single column or an array of
|
@@ -290,7 +461,7 @@ class ActiveRecord::Base
|
|
290
461
|
#
|
291
462
|
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], columns: [ :date_modified ] }
|
292
463
|
#
|
293
|
-
#
|
464
|
+
# ===== :index_predicate
|
294
465
|
#
|
295
466
|
# The :index_predicate attribute optionally specifies a WHERE condition
|
296
467
|
# on :conflict_target, which is required for matching against partial
|
@@ -299,7 +470,7 @@ class ActiveRecord::Base
|
|
299
470
|
#
|
300
471
|
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [ :author_id, :slug ], index_predicate: 'status <> 0', columns: [ :date_modified ] }
|
301
472
|
#
|
302
|
-
#
|
473
|
+
# ===== :constraint_name
|
303
474
|
#
|
304
475
|
# The :constraint_name attribute explicitly identifies the conflicting
|
305
476
|
# unique index by name. Postgres documentation discourages using this method
|
@@ -307,11 +478,28 @@ class ActiveRecord::Base
|
|
307
478
|
#
|
308
479
|
# BlogPost.import columns, values, on_duplicate_key_update: { constraint_name: :blog_posts_pkey, columns: [ :date_modified ] }
|
309
480
|
#
|
310
|
-
#
|
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.
|
311
493
|
#
|
312
|
-
#
|
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:
|
313
499
|
#
|
314
|
-
#
|
500
|
+
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
|
501
|
+
#
|
502
|
+
# ===== Using an Array
|
315
503
|
#
|
316
504
|
# The :columns attribute can be an array of column names. The column names
|
317
505
|
# are the only fields that are updated if a duplicate record is found.
|
@@ -319,7 +507,7 @@ class ActiveRecord::Base
|
|
319
507
|
#
|
320
508
|
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: [ :date_modified, :content, :author ] }
|
321
509
|
#
|
322
|
-
#
|
510
|
+
# ===== Using a Hash
|
323
511
|
#
|
324
512
|
# The :columns option can be a hash of column names to model attribute name
|
325
513
|
# mappings. This gives you finer grained control over what fields are updated
|
@@ -332,7 +520,8 @@ class ActiveRecord::Base
|
|
332
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.
|
333
521
|
# * num_inserts - the number of insert statements it took to import the data
|
334
522
|
# * ids - the primary keys of the imported ids if the adapter supports it, otherwise an empty array.
|
335
|
-
|
523
|
+
# * results - import results if the adapter supports it, otherwise an empty array.
|
524
|
+
def bulk_import(*args)
|
336
525
|
if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
|
337
526
|
options = {}
|
338
527
|
options.merge!( args.pop ) if args.last.is_a?(Hash)
|
@@ -343,31 +532,29 @@ class ActiveRecord::Base
|
|
343
532
|
import_helper(*args)
|
344
533
|
end
|
345
534
|
end
|
535
|
+
alias import bulk_import unless ActiveRecord::Base.respond_to? :import
|
346
536
|
|
347
537
|
# Imports a collection of values if all values are valid. Import fails at the
|
348
538
|
# first encountered validation error and raises ActiveRecord::RecordInvalid
|
349
539
|
# with the failed instance.
|
350
|
-
def
|
540
|
+
def bulk_import!(*args)
|
351
541
|
options = args.last.is_a?( Hash ) ? args.pop : {}
|
352
542
|
options[:validate] = true
|
353
543
|
options[:raise_error] = true
|
354
544
|
|
355
|
-
|
545
|
+
bulk_import(*args, options)
|
356
546
|
end
|
547
|
+
alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
|
357
548
|
|
358
549
|
def import_helper( *args )
|
359
|
-
options = { validate: true, timestamps: true }
|
550
|
+
options = { validate: true, timestamps: true, track_validation_failures: false }
|
360
551
|
options.merge!( args.pop ) if args.last.is_a? Hash
|
361
552
|
# making sure that current model's primary key is used
|
362
553
|
options[:primary_key] = primary_key
|
554
|
+
options[:locking_column] = locking_column if attribute_names.include?(locking_column)
|
363
555
|
|
364
|
-
|
365
|
-
|
366
|
-
options[:on_duplicate_key_update] = options[:on_duplicate_key_update].dup
|
367
|
-
end
|
368
|
-
|
369
|
-
is_validating = options[:validate]
|
370
|
-
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)
|
371
558
|
|
372
559
|
# assume array of model objects
|
373
560
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
@@ -376,23 +563,48 @@ class ActiveRecord::Base
|
|
376
563
|
column_names = args.first.dup
|
377
564
|
else
|
378
565
|
models = args.first
|
379
|
-
column_names =
|
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
|
380
571
|
end
|
381
572
|
|
382
|
-
if models.first.id.nil?
|
383
|
-
|
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
|
384
579
|
end
|
385
580
|
|
386
|
-
|
387
|
-
|
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
|
587
|
+
end
|
388
588
|
|
389
|
-
array_of_attributes =
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
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
|
394
606
|
else
|
395
|
-
model.
|
607
|
+
model.read_attribute(name.to_s)
|
396
608
|
end
|
397
609
|
end
|
398
610
|
end
|
@@ -401,19 +613,25 @@ class ActiveRecord::Base
|
|
401
613
|
if args.length == 2
|
402
614
|
array_of_hashes = args.last
|
403
615
|
column_names = args.first.dup
|
616
|
+
allow_extra_hash_keys = true
|
404
617
|
else
|
405
618
|
array_of_hashes = args.first
|
406
619
|
column_names = array_of_hashes.first.keys
|
620
|
+
allow_extra_hash_keys = false
|
407
621
|
end
|
408
622
|
|
409
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
|
+
|
410
628
|
column_names.map do |key|
|
411
629
|
h[key]
|
412
630
|
end
|
413
631
|
end
|
414
632
|
# supports empty array
|
415
633
|
elsif args.last.is_a?( Array ) && args.last.empty?
|
416
|
-
return ActiveRecord::Import::Result.new([], 0, [])
|
634
|
+
return ActiveRecord::Import::Result.new([], 0, [], [])
|
417
635
|
# supports 2-element array and array
|
418
636
|
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
419
637
|
|
@@ -438,47 +656,82 @@ class ActiveRecord::Base
|
|
438
656
|
|
439
657
|
if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && connection.prefetch_primary_key? && sequence_name
|
440
658
|
column_count = column_names.size
|
441
|
-
column_names.concat(primary_key).uniq!
|
659
|
+
column_names.concat(Array(primary_key)).uniq!
|
442
660
|
columns_added = column_names.size - column_count
|
443
661
|
new_fields = Array.new(columns_added)
|
444
662
|
array_of_attributes.each { |a| a.concat(new_fields) }
|
445
663
|
end
|
446
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
|
686
|
+
end
|
687
|
+
|
447
688
|
timestamps = {}
|
448
689
|
|
449
690
|
# record timestamps unless disabled in ActiveRecord::Base
|
450
|
-
if record_timestamps && options
|
691
|
+
if record_timestamps && options[:timestamps]
|
451
692
|
timestamps = add_special_rails_stamps column_names, array_of_attributes, options
|
452
693
|
end
|
453
694
|
|
454
695
|
return_obj = if is_validating
|
455
|
-
|
456
|
-
|
457
|
-
models.
|
458
|
-
|
459
|
-
|
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)
|
460
712
|
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
713
|
+
|
461
714
|
array_of_attributes[i] = nil
|
462
|
-
|
715
|
+
failure = model.dup
|
716
|
+
failure.errors.send(:initialize_dup, model.errors)
|
717
|
+
failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
|
463
718
|
end
|
719
|
+
array_of_attributes.compact!
|
464
720
|
end
|
465
|
-
else
|
466
|
-
import_with_validations( column_names, array_of_attributes, options )
|
467
721
|
end
|
468
722
|
else
|
469
|
-
|
470
|
-
ActiveRecord::Import::Result.new([], num_inserts, ids)
|
723
|
+
import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
471
724
|
end
|
472
725
|
|
473
726
|
if options[:synchronize]
|
474
|
-
sync_keys = options[:synchronize_keys] ||
|
727
|
+
sync_keys = options[:synchronize_keys] || Array(primary_key)
|
475
728
|
synchronize( options[:synchronize], sync_keys)
|
476
729
|
end
|
477
730
|
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
478
731
|
|
479
732
|
# if we have ids, then set the id on the models and mark the models as clean.
|
480
|
-
if models &&
|
481
|
-
set_attributes_and_mark_clean(models, return_obj, timestamps)
|
733
|
+
if models && supports_setting_primary_key_of_imported_objects?
|
734
|
+
set_attributes_and_mark_clean(models, return_obj, timestamps, options)
|
482
735
|
|
483
736
|
# if there are auto-save associations on the models we imported that are new, import them as well
|
484
737
|
import_associations(models, options.dup) if options[:recursive]
|
@@ -487,10 +740,6 @@ class ActiveRecord::Base
|
|
487
740
|
return_obj
|
488
741
|
end
|
489
742
|
|
490
|
-
# TODO import_from_table needs to be implemented.
|
491
|
-
def import_from_table( options ) # :nodoc:
|
492
|
-
end
|
493
|
-
|
494
743
|
# Imports the passed in +column_names+ and +array_of_attributes+
|
495
744
|
# given the passed in +options+ Hash with validations. Returns an
|
496
745
|
# object with the methods +failed_instances+ and +num_inserts+.
|
@@ -501,34 +750,14 @@ class ActiveRecord::Base
|
|
501
750
|
def import_with_validations( column_names, array_of_attributes, options = {} )
|
502
751
|
failed_instances = []
|
503
752
|
|
504
|
-
if block_given?
|
505
|
-
yield failed_instances
|
506
|
-
else
|
507
|
-
# create instances for each of our column/value sets
|
508
|
-
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
509
|
-
|
510
|
-
# keep track of the instance and the position it is currently at. if this fails
|
511
|
-
# validation we'll use the index to remove it from the array_of_attributes
|
512
|
-
model = new
|
513
|
-
arr.each_with_index do |hsh, i|
|
514
|
-
hsh.each_pair { |k, v| model[k] = v }
|
515
|
-
next if model.valid?(options[:validate_with_context])
|
516
|
-
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
517
|
-
array_of_attributes[i] = nil
|
518
|
-
failure = model.dup
|
519
|
-
failure.errors.send(:initialize_dup, model.errors)
|
520
|
-
failed_instances << failure
|
521
|
-
end
|
522
|
-
end
|
753
|
+
yield failed_instances if block_given?
|
523
754
|
|
524
|
-
|
525
|
-
|
526
|
-
num_inserts, ids = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
|
527
|
-
[0, []]
|
755
|
+
result = if options[:all_or_none] && failed_instances.any?
|
756
|
+
ActiveRecord::Import::Result.new([], 0, [], [])
|
528
757
|
else
|
529
758
|
import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
530
759
|
end
|
531
|
-
ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids)
|
760
|
+
ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results)
|
532
761
|
end
|
533
762
|
|
534
763
|
# Imports the passed in +column_names+ and +array_of_attributes+
|
@@ -538,6 +767,8 @@ class ActiveRecord::Base
|
|
538
767
|
# information on +column_names+, +array_of_attributes_ and
|
539
768
|
# +options+.
|
540
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
|
+
|
541
772
|
column_names = column_names.map(&:to_sym)
|
542
773
|
scope_columns, scope_values = scope_attributes.to_a.transpose
|
543
774
|
|
@@ -547,7 +778,7 @@ class ActiveRecord::Base
|
|
547
778
|
next if column_names.include?(name_as_sym)
|
548
779
|
|
549
780
|
is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
|
550
|
-
value = value.first if is_sti
|
781
|
+
value = Array(value).first if is_sti
|
551
782
|
|
552
783
|
column_names << name_as_sym
|
553
784
|
array_of_attributes.each { |attrs| attrs << value }
|
@@ -570,19 +801,33 @@ class ActiveRecord::Base
|
|
570
801
|
|
571
802
|
number_inserted = 0
|
572
803
|
ids = []
|
804
|
+
results = []
|
573
805
|
if supports_import?
|
574
806
|
# generate the sql
|
575
807
|
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
808
|
+
import_size = values_sql.size
|
809
|
+
|
810
|
+
batch_size = options[:batch_size] || import_size
|
811
|
+
run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
|
812
|
+
progress_proc = options[:batch_progress]
|
813
|
+
current_batch = 0
|
814
|
+
batches = (import_size / batch_size.to_f).ceil
|
576
815
|
|
577
|
-
batch_size = options[:batch_size] || values_sql.size
|
578
816
|
values_sql.each_slice(batch_size) do |batch_values|
|
817
|
+
batch_started_at = Time.now.to_i
|
818
|
+
|
579
819
|
# perform the inserts
|
580
820
|
result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
|
581
821
|
batch_values,
|
582
822
|
options,
|
583
|
-
"#{
|
584
|
-
|
585
|
-
|
823
|
+
"#{model_name} Create Many" )
|
824
|
+
|
825
|
+
number_inserted += result.num_inserts
|
826
|
+
ids += result.ids
|
827
|
+
results += result.results
|
828
|
+
current_batch += 1
|
829
|
+
|
830
|
+
progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
|
586
831
|
end
|
587
832
|
else
|
588
833
|
transaction(requires_new: true) do
|
@@ -592,27 +837,85 @@ class ActiveRecord::Base
|
|
592
837
|
end
|
593
838
|
end
|
594
839
|
end
|
595
|
-
[number_inserted, ids
|
840
|
+
ActiveRecord::Import::Result.new([], number_inserted, ids, results)
|
596
841
|
end
|
597
842
|
|
598
843
|
private
|
599
844
|
|
600
|
-
def set_attributes_and_mark_clean(models, import_result, timestamps)
|
845
|
+
def set_attributes_and_mark_clean(models, import_result, timestamps, options)
|
601
846
|
return if models.nil?
|
602
847
|
models -= import_result.failed_instances
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
848
|
+
|
849
|
+
# if ids were returned for all models we know all were updated
|
850
|
+
if models.size == import_result.ids.size
|
851
|
+
import_result.ids.each_with_index do |id, index|
|
852
|
+
model = models[index]
|
853
|
+
model.id = id
|
854
|
+
|
855
|
+
timestamps.each do |attr, value|
|
856
|
+
model.send(attr + "=", value)
|
857
|
+
end
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
deserialize_value = lambda do |column, value|
|
862
|
+
column = columns_hash[column]
|
863
|
+
return value unless column
|
864
|
+
if respond_to?(:type_caster)
|
865
|
+
type = type_for_attribute(column.name)
|
866
|
+
type.deserialize(value)
|
867
|
+
elsif column.respond_to?(:type_cast_from_database)
|
868
|
+
column.type_cast_from_database(value)
|
869
|
+
else
|
870
|
+
value
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
874
|
+
if models.size == import_result.results.size
|
875
|
+
columns = Array(options[:returning])
|
876
|
+
single_column = "#{columns.first}=" if columns.size == 1
|
877
|
+
import_result.results.each_with_index do |result, index|
|
878
|
+
model = models[index]
|
879
|
+
|
880
|
+
if single_column
|
881
|
+
val = deserialize_value.call(columns.first, result)
|
882
|
+
model.send(single_column, val)
|
883
|
+
else
|
884
|
+
columns.each_with_index do |column, col_index|
|
885
|
+
val = deserialize_value.call(column, result[col_index])
|
886
|
+
model.send("#{column}=", val)
|
887
|
+
end
|
888
|
+
end
|
889
|
+
end
|
890
|
+
end
|
891
|
+
|
892
|
+
models.each do |model|
|
893
|
+
if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
|
894
|
+
model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
|
895
|
+
model.changes_applied
|
896
|
+
elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
|
607
897
|
model.clear_changes_information
|
608
898
|
else # Rails 3.2
|
609
899
|
model.instance_variable_get(:@changed_attributes).clear
|
610
900
|
end
|
611
901
|
model.instance_variable_set(:@new_record, false)
|
902
|
+
end
|
903
|
+
end
|
612
904
|
|
613
|
-
|
614
|
-
|
615
|
-
|
905
|
+
# Sync belongs_to association ids with foreign key field
|
906
|
+
def load_association_ids(model)
|
907
|
+
changed_columns = model.changed
|
908
|
+
association_reflections = model.class.reflect_on_all_associations(:belongs_to)
|
909
|
+
association_reflections.each do |association_reflection|
|
910
|
+
column_name = association_reflection.foreign_key
|
911
|
+
next if association_reflection.options[:polymorphic]
|
912
|
+
next if changed_columns.include?(column_name)
|
913
|
+
association = model.association(association_reflection.name)
|
914
|
+
association = association.target
|
915
|
+
next if association.blank? || model.public_send(column_name).present?
|
916
|
+
|
917
|
+
association_primary_key = association_reflection.association_primary_key
|
918
|
+
model.public_send("#{column_name}=", association.send(association_primary_key))
|
616
919
|
end
|
617
920
|
end
|
618
921
|
|
@@ -625,12 +928,13 @@ class ActiveRecord::Base
|
|
625
928
|
associated_objects_by_class = {}
|
626
929
|
models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
|
627
930
|
|
628
|
-
# :on_duplicate_key_update not supported for associations
|
931
|
+
# :on_duplicate_key_update and :returning not supported for associations
|
629
932
|
options.delete(:on_duplicate_key_update)
|
933
|
+
options.delete(:returning)
|
630
934
|
|
631
935
|
associated_objects_by_class.each_value do |associations|
|
632
936
|
associations.each_value do |associated_records|
|
633
|
-
associated_records.first.class.
|
937
|
+
associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
|
634
938
|
end
|
635
939
|
end
|
636
940
|
end
|
@@ -639,6 +943,7 @@ class ActiveRecord::Base
|
|
639
943
|
# of class => objects to import.
|
640
944
|
def find_associated_objects_for_import(associated_objects_by_class, model)
|
641
945
|
associated_objects_by_class[model.class.name] ||= {}
|
946
|
+
return associated_objects_by_class unless model.id
|
642
947
|
|
643
948
|
association_reflections =
|
644
949
|
model.class.reflect_on_all_associations(:has_one) +
|
@@ -668,29 +973,36 @@ class ActiveRecord::Base
|
|
668
973
|
# Returns SQL the VALUES for an INSERT statement given the passed in +columns+
|
669
974
|
# and +array_of_attributes+.
|
670
975
|
def values_sql_for_columns_and_attributes(columns, array_of_attributes) # :nodoc:
|
671
|
-
# connection
|
672
|
-
# Reuse the same
|
976
|
+
# connection gets called a *lot* in this high intensity loop.
|
977
|
+
# Reuse the same one w/in the loop, otherwise it would keep being re-retreived (= lots of time for large imports)
|
673
978
|
connection_memo = connection
|
674
|
-
type_caster_memo = type_caster if respond_to?(:type_caster)
|
675
979
|
|
676
980
|
array_of_attributes.map do |arr|
|
677
981
|
my_values = arr.each_with_index.map do |val, j|
|
678
982
|
column = columns[j]
|
679
983
|
|
680
984
|
# be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
|
681
|
-
if val.nil? &&
|
985
|
+
if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
|
682
986
|
connection_memo.next_value_for_sequence(sequence_name)
|
987
|
+
elsif val.respond_to?(:to_sql)
|
988
|
+
"(#{val.to_sql})"
|
683
989
|
elsif column
|
684
|
-
if
|
685
|
-
|
686
|
-
|
990
|
+
if respond_to?(:type_caster) # Rails 5.0 and higher
|
991
|
+
type = type_for_attribute(column.name)
|
992
|
+
val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
|
993
|
+
connection_memo.quote(val)
|
994
|
+
elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
|
687
995
|
connection_memo.quote(column.type_cast_from_user(val), column)
|
688
|
-
else
|
996
|
+
else # Rails 3.2, 4.0 and 4.1
|
689
997
|
if serialized_attributes.include?(column.name)
|
690
998
|
val = serialized_attributes[column.name].dump(val)
|
691
999
|
end
|
692
|
-
|
1000
|
+
# Fixes #443 to support binary (i.e. bytea) columns on PG
|
1001
|
+
val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
|
1002
|
+
connection_memo.quote(val, column)
|
693
1003
|
end
|
1004
|
+
else
|
1005
|
+
raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
|
694
1006
|
end
|
695
1007
|
end
|
696
1008
|
"(#{my_values.join(',')})"
|
@@ -698,39 +1010,38 @@ class ActiveRecord::Base
|
|
698
1010
|
end
|
699
1011
|
|
700
1012
|
def add_special_rails_stamps( column_names, array_of_attributes, options )
|
701
|
-
|
1013
|
+
timestamp_columns = {}
|
1014
|
+
timestamps = {}
|
702
1015
|
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
# replace every instance of the array of attributes with our value
|
711
|
-
array_of_attributes.each { |arr| arr[index] = value if arr[index].nil? }
|
712
|
-
else
|
713
|
-
column_names << key
|
714
|
-
array_of_attributes.each { |arr| arr << value }
|
715
|
-
end
|
1016
|
+
if respond_to?(:all_timestamp_attributes_in_model, true) # Rails 5.1 and higher
|
1017
|
+
timestamp_columns[:create] = timestamp_attributes_for_create_in_model
|
1018
|
+
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
1019
|
+
else
|
1020
|
+
instance = allocate
|
1021
|
+
timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
|
1022
|
+
timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
|
716
1023
|
end
|
717
1024
|
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
1025
|
+
# use tz as set in ActiveRecord::Base
|
1026
|
+
timestamp = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
|
1027
|
+
|
1028
|
+
[:create, :update].each do |action|
|
1029
|
+
timestamp_columns[action].each do |column|
|
1030
|
+
column = column.to_s
|
1031
|
+
timestamps[column] = timestamp
|
1032
|
+
|
1033
|
+
index = column_names.index(column) || column_names.index(column.to_sym)
|
1034
|
+
if index
|
1035
|
+
# replace every instance of the array of attributes with our value
|
1036
|
+
array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
|
1037
|
+
else
|
1038
|
+
column_names << column
|
1039
|
+
array_of_attributes.each { |arr| arr << timestamp }
|
1040
|
+
end
|
731
1041
|
|
732
|
-
|
733
|
-
|
1042
|
+
if supports_on_duplicate_key_update? && action == :update
|
1043
|
+
connection.add_column_for_on_duplicate_key_update(column, options)
|
1044
|
+
end
|
734
1045
|
end
|
735
1046
|
end
|
736
1047
|
|
@@ -741,5 +1052,42 @@ class ActiveRecord::Base
|
|
741
1052
|
def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
|
742
1053
|
array_of_attributes.map { |values| Hash[column_names.zip(values)] }
|
743
1054
|
end
|
1055
|
+
|
1056
|
+
# Checks that the imported hash has the required_keys, optionally also checks that the hash has
|
1057
|
+
# no keys beyond those required when `allow_extra_keys` is false.
|
1058
|
+
# returns `nil` if validation passes, or an error message if it fails
|
1059
|
+
def validate_hash_import(hash, required_keys, allow_extra_keys) # :nodoc:
|
1060
|
+
extra_keys = allow_extra_keys ? [] : hash.keys - required_keys
|
1061
|
+
missing_keys = required_keys - hash.keys
|
1062
|
+
|
1063
|
+
return nil if extra_keys.empty? && missing_keys.empty?
|
1064
|
+
|
1065
|
+
if allow_extra_keys
|
1066
|
+
<<-EOS
|
1067
|
+
Hash key mismatch.
|
1068
|
+
|
1069
|
+
When importing an array of hashes with provided columns_names, each hash must contain keys for all column_names.
|
1070
|
+
|
1071
|
+
Required keys: #{required_keys}
|
1072
|
+
Missing keys: #{missing_keys}
|
1073
|
+
|
1074
|
+
Hash: #{hash}
|
1075
|
+
EOS
|
1076
|
+
else
|
1077
|
+
<<-EOS
|
1078
|
+
Hash key mismatch.
|
1079
|
+
|
1080
|
+
When importing an array of hashes, all hashes must have the same keys.
|
1081
|
+
If you have records that are missing some values, we recommend you either set default values
|
1082
|
+
for the missing keys or group these records into batches by key set before importing.
|
1083
|
+
|
1084
|
+
Required keys: #{required_keys}
|
1085
|
+
Extra keys: #{extra_keys}
|
1086
|
+
Missing keys: #{missing_keys}
|
1087
|
+
|
1088
|
+
Hash: #{hash}
|
1089
|
+
EOS
|
1090
|
+
end
|
1091
|
+
end
|
744
1092
|
end
|
745
1093
|
end
|