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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +49 -0
- data/.rubocop_todo.yml +36 -0
- data/.travis.yml +64 -8
- data/CHANGELOG.md +475 -0
- data/Gemfile +32 -15
- data/LICENSE +21 -56
- data/README.markdown +564 -35
- data/Rakefile +20 -3
- data/activerecord-import.gemspec +7 -7
- data/benchmarks/README +2 -2
- data/benchmarks/benchmark.rb +68 -64
- data/benchmarks/lib/base.rb +138 -137
- data/benchmarks/lib/cli_parser.rb +107 -103
- data/benchmarks/lib/{mysql_benchmark.rb → mysql2_benchmark.rb} +19 -22
- data/benchmarks/lib/output_to_csv.rb +5 -4
- data/benchmarks/lib/output_to_html.rb +8 -13
- data/benchmarks/models/test_innodb.rb +1 -1
- data/benchmarks/models/test_memory.rb +1 -1
- data/benchmarks/models/test_myisam.rb +1 -1
- data/benchmarks/schema/mysql2_schema.rb +16 -0
- data/gemfiles/3.2.gemfile +2 -4
- data/gemfiles/4.0.gemfile +2 -4
- data/gemfiles/4.1.gemfile +2 -4
- data/gemfiles/4.2.gemfile +2 -4
- data/gemfiles/5.0.gemfile +2 -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/active_record/adapters/jdbcsqlite3_adapter.rb +6 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +0 -1
- data/lib/activerecord-import/adapters/abstract_adapter.rb +23 -17
- data/lib/activerecord-import/adapters/mysql_adapter.rb +52 -25
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +187 -10
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +148 -17
- data/lib/activerecord-import/base.rb +15 -9
- data/lib/activerecord-import/import.rb +740 -191
- data/lib/activerecord-import/synchronize.rb +21 -21
- data/lib/activerecord-import/value_sets_parser.rb +33 -8
- data/lib/activerecord-import/version.rb +1 -1
- data/lib/activerecord-import.rb +4 -15
- data/test/adapters/jdbcsqlite3.rb +1 -0
- data/test/adapters/makara_postgis.rb +1 -0
- data/test/adapters/mysql2_makara.rb +1 -0
- data/test/adapters/mysql2spatial.rb +1 -1
- data/test/adapters/postgis.rb +1 -1
- data/test/adapters/postgresql.rb +1 -1
- data/test/adapters/postgresql_makara.rb +1 -0
- data/test/adapters/spatialite.rb +1 -1
- data/test/adapters/sqlite3.rb +1 -1
- data/test/database.yml.sample +13 -18
- data/test/import_test.rb +608 -89
- data/test/jdbcmysql/import_test.rb +2 -3
- data/test/jdbcpostgresql/import_test.rb +0 -2
- data/test/jdbcsqlite3/import_test.rb +4 -0
- data/test/makara_postgis/import_test.rb +8 -0
- data/test/models/account.rb +3 -0
- data/test/models/alarm.rb +2 -0
- data/test/models/animal.rb +6 -0
- data/test/models/bike_maker.rb +7 -0
- data/test/models/book.rb +7 -6
- data/test/models/car.rb +3 -0
- data/test/models/chapter.rb +2 -2
- data/test/models/dictionary.rb +4 -0
- data/test/models/discount.rb +3 -0
- data/test/models/end_note.rb +2 -2
- data/test/models/promotion.rb +3 -0
- data/test/models/question.rb +3 -0
- data/test/models/rule.rb +3 -0
- data/test/models/tag.rb +4 -0
- data/test/models/topic.rb +17 -3
- data/test/models/user.rb +3 -0
- data/test/models/user_token.rb +4 -0
- data/test/models/vendor.rb +7 -0
- data/test/models/widget.rb +19 -2
- data/test/mysql2/import_test.rb +2 -3
- data/test/{em_mysql2 → mysql2_makara}/import_test.rb +1 -1
- data/test/mysqlspatial2/import_test.rb +2 -2
- data/test/postgis/import_test.rb +5 -1
- data/test/schema/generic_schema.rb +159 -85
- data/test/schema/jdbcpostgresql_schema.rb +1 -0
- data/test/schema/mysql2_schema.rb +19 -0
- data/test/schema/postgis_schema.rb +1 -0
- data/test/schema/postgresql_schema.rb +61 -0
- data/test/schema/sqlite3_schema.rb +13 -0
- data/test/sqlite3/import_test.rb +2 -50
- data/test/support/active_support/test_case_extensions.rb +21 -13
- data/test/support/{mysql/assertions.rb → assertions.rb} +20 -2
- data/test/support/factories.rb +39 -14
- data/test/support/generate.rb +10 -10
- data/test/support/mysql/import_examples.rb +49 -98
- data/test/support/postgresql/import_examples.rb +535 -57
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +43 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +378 -0
- data/test/support/shared_examples/recursive_import.rb +225 -0
- data/test/support/sqlite3/import_examples.rb +231 -0
- data/test/synchronize_test.rb +10 -2
- data/test/test_helper.rb +36 -8
- data/test/travis/database.yml +26 -17
- data/test/value_sets_bytes_parser_test.rb +25 -17
- data/test/value_sets_records_parser_test.rb +6 -6
- metadata +86 -42
- data/benchmarks/boot.rb +0 -18
- data/benchmarks/schema/mysql_schema.rb +0 -16
- data/gemfiles/3.1.gemfile +0 -4
- data/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb +0 -8
- data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +0 -6
- data/lib/activerecord-import/em_mysql2.rb +0 -7
- data/lib/activerecord-import/mysql.rb +0 -7
- data/test/adapters/em_mysql2.rb +0 -1
- data/test/adapters/mysql.rb +0 -1
- data/test/adapters/mysqlspatial.rb +0 -1
- data/test/mysql/import_test.rb +0 -6
- data/test/mysqlspatial/import_test.rb +0 -6
- data/test/schema/mysql_schema.rb +0 -18
- data/test/travis/build.sh +0 -30
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
require "ostruct"
|
|
2
2
|
|
|
3
|
-
module ActiveRecord::Import::ConnectionAdapters
|
|
3
|
+
module ActiveRecord::Import::ConnectionAdapters; end
|
|
4
4
|
|
|
5
5
|
module ActiveRecord::Import #:nodoc:
|
|
6
|
-
|
|
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
|
|
30
|
-
@association.
|
|
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
|
|
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 =
|
|
43
|
-
symbolized_foreign_key =
|
|
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
|
-
|
|
47
|
-
|
|
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 )
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 )
|
|
71
|
-
return ActiveRecord::Import::Result.new([], 0, [])
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
240
|
+
return model_klass.bulk_import column_names, array_of_attributes, options
|
|
87
241
|
else
|
|
88
|
-
raise ArgumentError
|
|
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
|
|
126
|
-
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?
|
|
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
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
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
|
|
179
|
-
# if the adapter supports setting the primary keys of the
|
|
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 :
|
|
187
|
-
# BlogPost.new :
|
|
188
|
-
# BlogPost.new :
|
|
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, :
|
|
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, :
|
|
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(:
|
|
211
|
-
# BlogPost.import posts, :
|
|
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
|
|
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
|
-
#
|
|
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, :
|
|
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
|
|
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, :
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
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 = { :
|
|
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
|
-
|
|
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 )
|
|
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 =
|
|
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 =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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 )
|
|
279
|
-
return ActiveRecord::Import::Result.new([], 0, [])
|
|
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
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
300
|
-
|
|
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
|
-
|
|
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] ||
|
|
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
|
|
318
|
-
|
|
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
|
-
|
|
344
|
-
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
|
753
|
+
yield failed_instances if block_given?
|
|
345
754
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
408
|
-
(
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
418
|
-
|
|
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]
|
|
421
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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? &&
|
|
474
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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 <<
|
|
509
|
-
array_of_attributes.each { |arr| arr <<
|
|
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
|
-
|
|
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
|
|
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
|