activerecord-import 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +124 -0
- data/README.markdown +99 -32
- data/lib/activerecord-import/active_record/adapters/mysql2_proxy_adapter.rb +8 -0
- data/lib/activerecord-import/active_record/adapters/postgresql_proxy_adapter.rb +8 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_proxy_adapter.rb +8 -0
- data/lib/activerecord-import/active_record/adapters/trilogy_proxy_adapter.rb +8 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +58 -0
- data/lib/activerecord-import/adapters/active_record_proxy_adapter.rb +19 -0
- data/lib/activerecord-import/adapters/mysql2_proxy_adapter.rb +9 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +70 -13
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +10 -52
- data/lib/activerecord-import/adapters/postgresql_proxy_adapter.rb +9 -0
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -3
- data/lib/activerecord-import/adapters/sqlite3_proxy_adapter.rb +9 -0
- data/lib/activerecord-import/adapters/trilogy_proxy_adapter.rb +9 -0
- data/lib/activerecord-import/aliases.rb +42 -0
- data/lib/activerecord-import/base.rb +2 -46
- data/lib/activerecord-import/core.rb +62 -0
- data/lib/activerecord-import/import.rb +612 -369
- data/lib/activerecord-import/synchronize.rb +28 -16
- data/lib/activerecord-import/version.rb +1 -1
- data/lib/activerecord-import/without_aliases.rb +17 -0
- metadata +19 -211
- data/.github/workflows/test.yaml +0 -165
- data/.gitignore +0 -37
- data/.rubocop.yml +0 -118
- data/.rubocop_todo.yml +0 -30
- data/Brewfile +0 -5
- data/Dockerfile +0 -23
- data/Gemfile +0 -63
- data/Rakefile +0 -75
- data/activerecord-import.gemspec +0 -28
- data/benchmarks/README +0 -32
- data/benchmarks/benchmark.rb +0 -72
- data/benchmarks/lib/base.rb +0 -143
- data/benchmarks/lib/cli_parser.rb +0 -111
- data/benchmarks/lib/float.rb +0 -17
- data/benchmarks/lib/mysql2_benchmark.rb +0 -21
- data/benchmarks/lib/output_to_csv.rb +0 -21
- data/benchmarks/lib/output_to_html.rb +0 -66
- data/benchmarks/models/test_innodb.rb +0 -5
- data/benchmarks/models/test_memory.rb +0 -5
- data/benchmarks/models/test_myisam.rb +0 -5
- data/benchmarks/schema/mysql2_schema.rb +0 -18
- data/docker-compose.yml +0 -34
- data/gemfiles/5.2.gemfile +0 -4
- data/gemfiles/6.0.gemfile +0 -4
- data/gemfiles/6.1.gemfile +0 -4
- data/gemfiles/7.0.gemfile +0 -4
- data/gemfiles/7.1.gemfile +0 -3
- data/gemfiles/7.2.gemfile +0 -3
- data/gemfiles/8.0.gemfile +0 -3
- data/test/adapters/janus_mysql2.rb +0 -3
- data/test/adapters/janus_trilogy.rb +0 -11
- data/test/adapters/jdbcmysql.rb +0 -3
- data/test/adapters/jdbcpostgresql.rb +0 -3
- data/test/adapters/jdbcsqlite3.rb +0 -3
- data/test/adapters/makara_postgis.rb +0 -3
- data/test/adapters/mysql2.rb +0 -3
- data/test/adapters/mysql2_makara.rb +0 -3
- data/test/adapters/mysql2_proxy.rb +0 -3
- data/test/adapters/mysql2spatial.rb +0 -3
- data/test/adapters/postgis.rb +0 -3
- data/test/adapters/postgresql.rb +0 -3
- data/test/adapters/postgresql_makara.rb +0 -3
- data/test/adapters/postgresql_proxy.rb +0 -3
- data/test/adapters/seamless_database_pool.rb +0 -3
- data/test/adapters/spatialite.rb +0 -3
- data/test/adapters/sqlite3.rb +0 -3
- data/test/adapters/trilogy.rb +0 -11
- data/test/database.yml.sample +0 -73
- data/test/github/database.yml +0 -85
- data/test/import_test.rb +0 -970
- data/test/janus_mysql2/import_test.rb +0 -8
- data/test/janus_trilogy/import_test.rb +0 -7
- data/test/jdbcmysql/import_test.rb +0 -7
- data/test/jdbcpostgresql/import_test.rb +0 -6
- data/test/jdbcsqlite3/import_test.rb +0 -6
- data/test/makara_postgis/import_test.rb +0 -10
- data/test/models/account.rb +0 -5
- data/test/models/alarm.rb +0 -4
- data/test/models/animal.rb +0 -8
- data/test/models/author.rb +0 -9
- data/test/models/bike_maker.rb +0 -10
- data/test/models/book.rb +0 -18
- data/test/models/car.rb +0 -5
- data/test/models/card.rb +0 -5
- data/test/models/chapter.rb +0 -6
- data/test/models/composite_book.rb +0 -19
- data/test/models/composite_chapter.rb +0 -12
- data/test/models/customer.rb +0 -18
- data/test/models/deck.rb +0 -8
- data/test/models/dictionary.rb +0 -6
- data/test/models/discount.rb +0 -5
- data/test/models/end_note.rb +0 -6
- data/test/models/group.rb +0 -5
- data/test/models/order.rb +0 -17
- data/test/models/playing_card.rb +0 -4
- data/test/models/promotion.rb +0 -5
- data/test/models/question.rb +0 -5
- data/test/models/rule.rb +0 -5
- data/test/models/tag.rb +0 -12
- data/test/models/tag_alias.rb +0 -11
- data/test/models/topic.rb +0 -31
- data/test/models/user.rb +0 -5
- data/test/models/user_token.rb +0 -6
- data/test/models/vendor.rb +0 -9
- data/test/models/widget.rb +0 -33
- data/test/mysql2/import_test.rb +0 -7
- data/test/mysql2_makara/import_test.rb +0 -8
- data/test/mysql2_proxy/import_test.rb +0 -6
- data/test/mysqlspatial2/import_test.rb +0 -8
- data/test/postgis/import_test.rb +0 -10
- data/test/postgresql/import_test.rb +0 -6
- data/test/postgresql_proxy/import_test.rb +0 -6
- data/test/schema/generic_schema.rb +0 -230
- data/test/schema/jdbcpostgresql_schema.rb +0 -3
- data/test/schema/mysql2_schema.rb +0 -21
- data/test/schema/postgis_schema.rb +0 -3
- data/test/schema/postgresql_schema.rb +0 -94
- data/test/schema/sqlite3_schema.rb +0 -15
- data/test/schema/version.rb +0 -12
- data/test/sqlite3/import_test.rb +0 -6
- data/test/support/active_support/test_case_extensions.rb +0 -73
- data/test/support/assertions.rb +0 -75
- data/test/support/factories.rb +0 -66
- data/test/support/generate.rb +0 -31
- data/test/support/mysql/import_examples.rb +0 -97
- data/test/support/postgresql/import_examples.rb +0 -642
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +0 -45
- data/test/support/shared_examples/on_duplicate_key_update.rb +0 -437
- data/test/support/shared_examples/recursive_import.rb +0 -352
- data/test/support/sqlite3/import_examples.rb +0 -232
- data/test/synchronize_test.rb +0 -43
- data/test/test_helper.rb +0 -91
- data/test/trilogy/import_test.rb +0 -7
- data/test/value_sets_bytes_parser_test.rb +0 -106
- data/test/value_sets_records_parser_test.rb +0 -34
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module ActiveRecord::Import::ConnectionAdapters; end
|
|
4
4
|
|
|
5
5
|
module ActiveRecord::Import # :nodoc:
|
|
6
|
-
Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
|
|
6
|
+
Result = Struct.new(:failed_instances, :num_inserts, :ids, :results, :affected_rows)
|
|
7
7
|
|
|
8
8
|
module ImportSupport # :nodoc:
|
|
9
9
|
def supports_import? # :nodoc:
|
|
@@ -23,111 +23,84 @@ module ActiveRecord::Import # :nodoc:
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
# Skips uniqueness validations while a model is being validated for an import
|
|
27
|
+
# that doesn't validate uniqueness, including the validations Rails runs on
|
|
28
|
+
# the model's associated records.
|
|
29
|
+
module SkipUniquenessValidation # :nodoc:
|
|
30
|
+
def validate_each(record, attribute, value)
|
|
31
|
+
super unless ActiveRecord::Import::Validator.skip_uniqueness?
|
|
31
32
|
end
|
|
33
|
+
end
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
# Validates the presence of a belongs_to association on a model being
|
|
36
|
+
# validated for an import by checking its foreign key, so the associated
|
|
37
|
+
# record isn't loaded for every model.
|
|
38
|
+
module ForeignKeyPresenceValidation # :nodoc:
|
|
39
|
+
def validate(record)
|
|
40
|
+
validator = ActiveRecord::Import::Validator.foreign_key_presence_validator(self, record)
|
|
41
|
+
return super unless validator
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
(!@options[:validate_uniqueness] &&
|
|
40
|
-
filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
|
|
43
|
+
ActiveModel::EachValidator.instance_method(:validate).bind(validator).call(record)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
class Validator
|
|
48
|
+
# Holds [validator, model] for the current thread while valid_model? runs.
|
|
49
|
+
CURRENT = :activerecord_import_validation
|
|
45
50
|
|
|
46
|
-
|
|
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.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
|
|
53
|
-
attrs[index] = key unless attrs.include?(key)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
51
|
+
attr_reader :options
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
def self.skip_uniqueness?
|
|
54
|
+
validator, = Thread.current[CURRENT]
|
|
55
|
+
validator ? !validator.options[:validate_uniqueness] : false
|
|
56
|
+
end
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
callback.filter = callback.send(:_compile_filter, filter)
|
|
69
|
-
@validate_callbacks[i] = callback
|
|
70
|
-
end
|
|
71
|
-
end
|
|
58
|
+
def self.foreign_key_presence_validator(presence_validator, record)
|
|
59
|
+
validator, model = Thread.current[CURRENT]
|
|
60
|
+
validator.foreign_key_presence_validator(presence_validator, record.class) if model.equal?(record)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(options = {})
|
|
64
|
+
@options = options
|
|
65
|
+
@foreign_key_presence_validators = {}
|
|
72
66
|
end
|
|
73
67
|
|
|
74
68
|
def valid_model?(model)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if runner.respond_to?(:call) # ActiveRecord < 5.1
|
|
94
|
-
runner.call(env)
|
|
95
|
-
else # ActiveRecord >= 5.1
|
|
96
|
-
# Note that this is a gross simplification of ActiveSupport::Callbacks#run_callbacks.
|
|
97
|
-
# It's technically possible for there to exist an "around" callback in the
|
|
98
|
-
# :validate chain, but this would be an aberration, since Rails doesn't define
|
|
99
|
-
# "around_validate". Still, rather than silently ignoring such callbacks, we
|
|
100
|
-
# explicitly raise a RuntimeError, since activerecord-import was asked to perform
|
|
101
|
-
# validations and it's unable to do so.
|
|
102
|
-
#
|
|
103
|
-
# The alternative here would be to copy-and-paste the bulk of the
|
|
104
|
-
# ActiveSupport::Callbacks#run_callbacks method, which is undesirable if there's
|
|
105
|
-
# no real-world use case for it.
|
|
106
|
-
raise "The :validate callback chain contains an 'around' callback, which is unsupported" unless runner.final?
|
|
107
|
-
runner.invoke_before(env)
|
|
108
|
-
# Ensure a truthy value is returned. ActiveRecord < 7.2 always returned an array.
|
|
109
|
-
runner.invoke_after(env) || []
|
|
110
|
-
end
|
|
111
|
-
elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
|
112
|
-
model.instance_eval @validate_callbacks.compile
|
|
113
|
-
else # ActiveRecord 3.x
|
|
114
|
-
model.instance_eval @validate_callbacks.compile(nil, model)
|
|
115
|
-
end
|
|
69
|
+
current = Thread.current[CURRENT]
|
|
70
|
+
Thread.current[CURRENT] = [self, model]
|
|
71
|
+
model.valid?(@options[:validate_with_context])
|
|
72
|
+
ensure
|
|
73
|
+
Thread.current[CURRENT] = current
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns a copy of +presence_validator+ that validates the foreign keys of
|
|
77
|
+
# any belongs_to associations it validates, or nil if there are none.
|
|
78
|
+
def foreign_key_presence_validator(presence_validator, klass)
|
|
79
|
+
key = [presence_validator, klass]
|
|
80
|
+
return @foreign_key_presence_validators[key] if @foreign_key_presence_validators.key?(key)
|
|
81
|
+
|
|
82
|
+
attrs = presence_validator.attributes.dup
|
|
83
|
+
klass.reflect_on_all_associations(:belongs_to).each do |assoc|
|
|
84
|
+
if (index = attrs.index(assoc.name))
|
|
85
|
+
foreign_key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
|
|
86
|
+
attrs[index] = foreign_key unless attrs.include?(foreign_key)
|
|
116
87
|
end
|
|
88
|
+
end
|
|
117
89
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
model.send(:validation_context=, current_context)
|
|
90
|
+
@foreign_key_presence_validators[key] = if attrs != presence_validator.attributes
|
|
91
|
+
presence_validator.dup.tap { |validator| validator.instance_variable_set(:@attributes, attrs.flatten) }
|
|
121
92
|
end
|
|
122
93
|
end
|
|
123
94
|
end
|
|
124
95
|
end
|
|
125
96
|
|
|
97
|
+
ActiveRecord::Validations::UniquenessValidator.prepend(ActiveRecord::Import::SkipUniquenessValidation)
|
|
98
|
+
ActiveModel::Validations::PresenceValidator.prepend(ActiveRecord::Import::ForeignKeyPresenceValidation)
|
|
99
|
+
|
|
126
100
|
class ActiveRecord::Associations::CollectionProxy
|
|
127
101
|
def bulk_import(*args, &block)
|
|
128
102
|
@association.bulk_import(*args, &block)
|
|
129
103
|
end
|
|
130
|
-
alias import bulk_import unless respond_to? :import
|
|
131
104
|
end
|
|
132
105
|
|
|
133
106
|
class ActiveRecord::Associations::CollectionAssociation
|
|
@@ -139,9 +112,10 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
139
112
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
140
113
|
|
|
141
114
|
model_klass = reflection.klass
|
|
115
|
+
args = model_klass.send(:arrays_from_array_like_import_arguments, args)
|
|
142
116
|
symbolized_foreign_key = reflection.foreign_key.to_sym
|
|
143
117
|
|
|
144
|
-
symbolized_column_names = if model_klass.
|
|
118
|
+
symbolized_column_names = if model_klass.connection_pool.with_connection(&:supports_virtual_columns?)
|
|
145
119
|
model_klass.columns.reject(&:virtual?).map { |c| c.name.to_sym }
|
|
146
120
|
else
|
|
147
121
|
model_klass.column_names.map(&:to_sym)
|
|
@@ -149,12 +123,14 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
149
123
|
|
|
150
124
|
owner_primary_key = reflection.active_record_primary_key.to_sym
|
|
151
125
|
owner_primary_key_value = owner.send(owner_primary_key)
|
|
126
|
+
owner_polymorphic_name = owner.class.polymorphic_name if reflection.type
|
|
152
127
|
|
|
153
128
|
# assume array of model objects
|
|
154
129
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
|
155
130
|
if args.length == 2
|
|
156
131
|
models = args.last
|
|
157
132
|
column_names = args.first.dup
|
|
133
|
+
symbolized_column_names = column_names.map(&:to_sym)
|
|
158
134
|
else
|
|
159
135
|
models = args.first
|
|
160
136
|
column_names = symbolized_column_names
|
|
@@ -164,9 +140,13 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
164
140
|
column_names << symbolized_foreign_key
|
|
165
141
|
end
|
|
166
142
|
|
|
143
|
+
if reflection.type && !symbolized_column_names.include?(reflection.type.to_sym)
|
|
144
|
+
column_names << reflection.type.to_sym
|
|
145
|
+
end
|
|
146
|
+
|
|
167
147
|
models.each do |m|
|
|
168
148
|
m.public_send "#{symbolized_foreign_key}=", owner_primary_key_value
|
|
169
|
-
m.public_send "#{reflection.type}=",
|
|
149
|
+
m.public_send "#{reflection.type}=", owner_polymorphic_name if reflection.type
|
|
170
150
|
end
|
|
171
151
|
|
|
172
152
|
model_klass.bulk_import column_names, models, options
|
|
@@ -183,6 +163,7 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
183
163
|
allow_extra_hash_keys = false
|
|
184
164
|
end
|
|
185
165
|
|
|
166
|
+
required_column_names = column_names.dup
|
|
186
167
|
symbolized_column_names = column_names.map(&:to_sym)
|
|
187
168
|
unless symbolized_column_names.include?(symbolized_foreign_key)
|
|
188
169
|
column_names << symbolized_foreign_key
|
|
@@ -193,15 +174,16 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
193
174
|
end
|
|
194
175
|
|
|
195
176
|
array_of_attributes = array_of_hashes.map do |h|
|
|
196
|
-
error_message = model_klass.send(:validate_hash_import, h,
|
|
177
|
+
error_message = model_klass.send(:validate_hash_import, h, required_column_names, allow_extra_hash_keys)
|
|
197
178
|
|
|
198
179
|
raise ArgumentError, error_message if error_message
|
|
199
180
|
|
|
200
181
|
column_names.map do |key|
|
|
201
|
-
|
|
182
|
+
symbolized_key = key.to_sym
|
|
183
|
+
if symbolized_key == symbolized_foreign_key
|
|
202
184
|
owner_primary_key_value
|
|
203
|
-
elsif reflection.type &&
|
|
204
|
-
|
|
185
|
+
elsif reflection.type && symbolized_key == reflection.type.to_sym
|
|
186
|
+
owner_polymorphic_name
|
|
205
187
|
else
|
|
206
188
|
h[key]
|
|
207
189
|
end
|
|
@@ -236,51 +218,70 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
236
218
|
symbolized_type = reflection.type.to_sym
|
|
237
219
|
if symbolized_column_names.include?(symbolized_type)
|
|
238
220
|
index = symbolized_column_names.index(symbolized_type)
|
|
239
|
-
array_of_attributes.each { |attrs| attrs[index] =
|
|
221
|
+
array_of_attributes.each { |attrs| attrs[index] = owner_polymorphic_name }
|
|
240
222
|
else
|
|
241
223
|
column_names << symbolized_type
|
|
242
|
-
array_of_attributes.each { |attrs| attrs <<
|
|
224
|
+
array_of_attributes.each { |attrs| attrs << owner_polymorphic_name }
|
|
243
225
|
end
|
|
244
226
|
end
|
|
245
227
|
|
|
246
228
|
model_klass.bulk_import column_names, array_of_attributes, options
|
|
247
229
|
else
|
|
248
|
-
raise ArgumentError,
|
|
230
|
+
raise ArgumentError, model_klass.send(:invalid_import_arguments_message, args)
|
|
249
231
|
end
|
|
250
232
|
end
|
|
251
|
-
alias import bulk_import unless respond_to? :import
|
|
252
233
|
end
|
|
253
234
|
|
|
254
|
-
module ActiveRecord::Import::
|
|
255
|
-
def establish_connection(args
|
|
256
|
-
|
|
257
|
-
ActiveRecord::Import.load_from_connection_pool
|
|
258
|
-
|
|
235
|
+
module ActiveRecord::Import::ConnectionHandler
|
|
236
|
+
def establish_connection(*args, **kwargs, &block)
|
|
237
|
+
pool = super(*args, **kwargs, &block)
|
|
238
|
+
ActiveRecord::Import.load_from_connection_pool pool
|
|
239
|
+
pool
|
|
259
240
|
end
|
|
260
241
|
end
|
|
261
242
|
|
|
243
|
+
class ActiveRecord::ConnectionAdapters::ConnectionHandler
|
|
244
|
+
prepend ActiveRecord::Import::ConnectionHandler
|
|
245
|
+
end
|
|
246
|
+
|
|
262
247
|
class ActiveRecord::Base
|
|
263
248
|
class << self
|
|
264
|
-
prepend ActiveRecord::Import::Connection
|
|
265
|
-
|
|
266
249
|
# Returns true if the current database connection adapter
|
|
267
250
|
# supports import functionality, otherwise returns false.
|
|
268
|
-
def supports_import?(
|
|
269
|
-
|
|
251
|
+
def supports_import?(conn = nil)
|
|
252
|
+
if conn
|
|
253
|
+
conn.respond_to?(:supports_import?) && conn.supports_import?
|
|
254
|
+
else
|
|
255
|
+
connection_pool.with_connection do |conn|
|
|
256
|
+
conn.respond_to?(:supports_import?) && conn.supports_import?
|
|
257
|
+
end
|
|
258
|
+
end
|
|
270
259
|
end
|
|
271
260
|
|
|
272
261
|
# Returns true if the current database connection adapter
|
|
273
262
|
# supports on duplicate key update functionality, otherwise
|
|
274
263
|
# returns false.
|
|
275
|
-
def supports_on_duplicate_key_update?
|
|
276
|
-
|
|
264
|
+
def supports_on_duplicate_key_update?(conn = nil)
|
|
265
|
+
if conn
|
|
266
|
+
conn.respond_to?(:supports_on_duplicate_key_update?) && conn.supports_on_duplicate_key_update?
|
|
267
|
+
else
|
|
268
|
+
connection_pool.with_connection do |conn|
|
|
269
|
+
conn.respond_to?(:supports_on_duplicate_key_update?) && conn.supports_on_duplicate_key_update?
|
|
270
|
+
end
|
|
271
|
+
end
|
|
277
272
|
end
|
|
278
273
|
|
|
279
274
|
# returns true if the current database connection adapter
|
|
280
275
|
# supports setting the primary key of bulk imported models, otherwise
|
|
281
276
|
# returns false
|
|
282
|
-
def supports_setting_primary_key_of_imported_objects?
|
|
283
|
-
|
|
277
|
+
def supports_setting_primary_key_of_imported_objects?(conn = nil)
|
|
278
|
+
if conn
|
|
279
|
+
conn.respond_to?(:supports_setting_primary_key_of_imported_objects?) && conn.supports_setting_primary_key_of_imported_objects?
|
|
280
|
+
else
|
|
281
|
+
connection_pool.with_connection do |conn|
|
|
282
|
+
conn.respond_to?(:supports_setting_primary_key_of_imported_objects?) && conn.supports_setting_primary_key_of_imported_objects?
|
|
283
|
+
end
|
|
284
|
+
end
|
|
284
285
|
end
|
|
285
286
|
|
|
286
287
|
# Imports a collection of values to the database.
|
|
@@ -348,7 +349,7 @@ class ActiveRecord::Base
|
|
|
348
349
|
# (if false) even if record timestamps is disabled in ActiveRecord::Base
|
|
349
350
|
# * +recursive+ - true|false, tells import to import all has_many/has_one
|
|
350
351
|
# associations if the adapter supports setting the primary keys of the
|
|
351
|
-
# newly imported objects. PostgreSQL only.
|
|
352
|
+
# newly imported objects. PostgreSQL and MariaDB 10.5+ only.
|
|
352
353
|
# * +batch_size+ - an integer value to specify the max number of records to
|
|
353
354
|
# include per insert. Defaults to the total number of records to import.
|
|
354
355
|
#
|
|
@@ -403,7 +404,8 @@ class ActiveRecord::Base
|
|
|
403
404
|
# ==== Using :all
|
|
404
405
|
#
|
|
405
406
|
# The :on_duplicate_key_update option can be set to :all. All columns
|
|
406
|
-
# other than the primary key are updated
|
|
407
|
+
# other than the primary key are updated, except for timestamps recorded
|
|
408
|
+
# on create, such as created_at. If a list of column names is
|
|
407
409
|
# supplied, only those columns will be updated. Below is an example:
|
|
408
410
|
#
|
|
409
411
|
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
|
@@ -434,7 +436,8 @@ class ActiveRecord::Base
|
|
|
434
436
|
# ==== Using :all
|
|
435
437
|
#
|
|
436
438
|
# The :on_duplicate_key_update option can be set to :all. All columns
|
|
437
|
-
# other than the primary key are updated
|
|
439
|
+
# other than the primary key are updated, except for timestamps recorded
|
|
440
|
+
# on create, such as created_at. If a list of column names is
|
|
438
441
|
# supplied, only those columns will be updated. Below is an example:
|
|
439
442
|
#
|
|
440
443
|
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
|
@@ -499,7 +502,8 @@ class ActiveRecord::Base
|
|
|
499
502
|
#
|
|
500
503
|
# ===== Using :all
|
|
501
504
|
#
|
|
502
|
-
# The :columns attribute can be :all. All columns other than the primary key will be updated
|
|
505
|
+
# The :columns attribute can be :all. All columns other than the primary key will be updated,
|
|
506
|
+
# except for timestamps recorded on create, such as created_at.
|
|
503
507
|
# If a list of column names is supplied, only those columns will be updated.
|
|
504
508
|
# Below is an example:
|
|
505
509
|
#
|
|
@@ -538,219 +542,101 @@ class ActiveRecord::Base
|
|
|
538
542
|
import_helper(*args)
|
|
539
543
|
end
|
|
540
544
|
end
|
|
541
|
-
alias import bulk_import unless ActiveRecord::Base.respond_to? :import
|
|
542
545
|
|
|
543
546
|
# Imports a collection of values if all values are valid. Import fails at the
|
|
544
547
|
# first encountered validation error and raises ActiveRecord::RecordInvalid
|
|
545
548
|
# with the failed instance.
|
|
546
549
|
def bulk_import!(*args)
|
|
547
550
|
options = args.last.is_a?( Hash ) ? args.pop : {}
|
|
548
|
-
options
|
|
549
|
-
options[:raise_error] = true
|
|
551
|
+
options = options.merge( validate: true, raise_error: true )
|
|
550
552
|
|
|
551
553
|
bulk_import(*args, options)
|
|
552
554
|
end
|
|
553
|
-
alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
|
|
554
|
-
|
|
555
|
-
def import_helper( *args )
|
|
556
|
-
options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
|
|
557
|
-
options.merge!( args.pop ) if args.last.is_a? Hash
|
|
558
|
-
# making sure that current model's primary key is used
|
|
559
|
-
options[:primary_key] = primary_key
|
|
560
|
-
options[:locking_column] = locking_column if locking_enabled?
|
|
561
|
-
|
|
562
|
-
is_validating = options[:validate_with_context].present? || options[:validate]
|
|
563
|
-
validator = ActiveRecord::Import::Validator.new(self, options)
|
|
564
|
-
|
|
565
|
-
# assume array of model objects
|
|
566
|
-
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
|
567
|
-
if args.length == 2
|
|
568
|
-
models = args.last
|
|
569
|
-
column_names = args.first.dup
|
|
570
|
-
else
|
|
571
|
-
models = args.first
|
|
572
|
-
column_names = if connection.respond_to?(:supports_virtual_columns?) && connection.supports_virtual_columns?
|
|
573
|
-
columns.reject(&:virtual?).map(&:name)
|
|
574
|
-
else
|
|
575
|
-
self.column_names.dup
|
|
576
|
-
end
|
|
577
|
-
end
|
|
578
555
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
556
|
+
# Returns the column for +name+, following an attribute alias unless +name+ is
|
|
557
|
+
# itself a column, so a column that's also an alias isn't replaced by the
|
|
558
|
+
# column it aliases.
|
|
559
|
+
def column_name_for_import(name) # :nodoc:
|
|
560
|
+
name = name.to_s
|
|
561
|
+
attribute_alias?(name) && !columns_hash.key?(name) ? attribute_alias(name) : name
|
|
562
|
+
end
|
|
586
563
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
end
|
|
593
|
-
end
|
|
564
|
+
def import_helper( *args )
|
|
565
|
+
connection_pool.with_connection do |conn|
|
|
566
|
+
# The adapter's import support is loaded when the connection is established, which is
|
|
567
|
+
# missed if activerecord-import is required afterwards, so load it now if it's missing.
|
|
568
|
+
ActiveRecord::Import.load_from_connection_pool_once(connection_pool) unless supports_import?(conn)
|
|
594
569
|
|
|
595
|
-
|
|
570
|
+
options, args = options_and_arguments_for_import(args)
|
|
596
571
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
load_association_ids(model)
|
|
600
|
-
end
|
|
572
|
+
is_validating = options[:validate_with_context].present? || options[:validate]
|
|
573
|
+
validator = ActiveRecord::Import::Validator.new(options) if is_validating
|
|
601
574
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
next
|
|
605
|
-
end
|
|
575
|
+
# supports empty array
|
|
576
|
+
return ActiveRecord::Import::Result.new([], 0, [], []) if args.last.is_a?( Array ) && args.last.empty?
|
|
606
577
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
nil
|
|
612
|
-
else
|
|
613
|
-
model.read_attribute(name.to_s)
|
|
614
|
-
end
|
|
615
|
-
end
|
|
616
|
-
end
|
|
617
|
-
# supports array of hash objects
|
|
618
|
-
elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
|
|
619
|
-
if args.length == 2
|
|
620
|
-
array_of_hashes = args.last
|
|
621
|
-
column_names = args.first.dup
|
|
622
|
-
allow_extra_hash_keys = true
|
|
578
|
+
# assume array of model objects
|
|
579
|
+
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
|
580
|
+
models, column_names, default_column_names = models_and_column_names_for_import(conn, args)
|
|
581
|
+
array_of_attributes = attributes_from_models(conn, models, column_names, validator, options)
|
|
623
582
|
else
|
|
624
|
-
|
|
625
|
-
column_names = array_of_hashes.first.keys
|
|
626
|
-
allow_extra_hash_keys = false
|
|
583
|
+
column_names, array_of_attributes = column_names_and_attributes_for_import(args)
|
|
627
584
|
end
|
|
628
585
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
raise ArgumentError, error_message if error_message
|
|
633
|
-
|
|
634
|
-
column_names.map do |key|
|
|
635
|
-
h[key]
|
|
636
|
-
end
|
|
637
|
-
end
|
|
638
|
-
# supports empty array
|
|
639
|
-
elsif args.last.is_a?( Array ) && args.last.empty?
|
|
640
|
-
return ActiveRecord::Import::Result.new([], 0, [], [])
|
|
641
|
-
# supports 2-element array and array
|
|
642
|
-
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
|
586
|
+
# Rows without a value for these columns get the database's default rather than NULL
|
|
587
|
+
options[:database_default_columns] = Array(default_column_names) | database_populated_primary_keys(conn)
|
|
643
588
|
|
|
644
|
-
|
|
645
|
-
|
|
589
|
+
# Don't modify incoming arguments
|
|
590
|
+
on_duplicate_key_update = options[:on_duplicate_key_update]
|
|
591
|
+
if on_duplicate_key_update
|
|
592
|
+
options[:on_duplicate_key_update] = on_duplicate_key_update_for_import(on_duplicate_key_update, column_names, options)
|
|
646
593
|
end
|
|
647
594
|
|
|
648
|
-
column_names, array_of_attributes
|
|
595
|
+
add_primary_key_for_sequence(conn, column_names, array_of_attributes)
|
|
649
596
|
|
|
650
|
-
|
|
651
|
-
column_names = column_names.dup
|
|
652
|
-
array_of_attributes = array_of_attributes.map(&:dup)
|
|
653
|
-
else
|
|
654
|
-
raise ArgumentError, "Invalid arguments!"
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
# Force the primary key col into the insert if it's not
|
|
658
|
-
# on the list and we are using a sequence and stuff a nil
|
|
659
|
-
# value for it into each row so the sequencer will fire later
|
|
660
|
-
symbolized_column_names = Array(column_names).map(&:to_sym)
|
|
661
|
-
symbolized_primary_key = Array(primary_key).map(&:to_sym)
|
|
597
|
+
timestamps = {}
|
|
662
598
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
new_fields = Array.new(columns_added)
|
|
668
|
-
array_of_attributes.each { |a| a.concat(new_fields) }
|
|
669
|
-
end
|
|
599
|
+
# record timestamps unless disabled in ActiveRecord::Base
|
|
600
|
+
if record_timestamps && options[:timestamps]
|
|
601
|
+
timestamps = add_special_rails_stamps conn, column_names, array_of_attributes, options
|
|
602
|
+
end
|
|
670
603
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
|
|
676
|
-
on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
|
|
677
|
-
duped_options[k] = if k == :columns && v == :all
|
|
678
|
-
updatable_columns
|
|
679
|
-
elsif v.duplicable?
|
|
680
|
-
v.dup
|
|
604
|
+
return_obj = if is_validating
|
|
605
|
+
import_with_validations( conn, column_names, array_of_attributes, options ) do |failed_instances|
|
|
606
|
+
if models
|
|
607
|
+
add_failed_models(failed_instances, models, options)
|
|
681
608
|
else
|
|
682
|
-
|
|
609
|
+
validate_attributes_for_import(failed_instances, column_names, array_of_attributes, validator, options)
|
|
683
610
|
end
|
|
684
611
|
end
|
|
685
|
-
elsif on_duplicate_key_update == :all
|
|
686
|
-
updatable_columns
|
|
687
|
-
elsif on_duplicate_key_update.duplicable?
|
|
688
|
-
on_duplicate_key_update.dup
|
|
689
612
|
else
|
|
690
|
-
|
|
613
|
+
import_without_validations_or_callbacks( conn, column_names, array_of_attributes, options )
|
|
691
614
|
end
|
|
692
|
-
end
|
|
693
615
|
|
|
694
|
-
|
|
616
|
+
return return_obj if options[:all_or_none] && return_obj.failed_instances.any?
|
|
695
617
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
618
|
+
if options[:synchronize]
|
|
619
|
+
sync_keys = options[:synchronize_keys] || Array(primary_key)
|
|
620
|
+
synchronized_instances = synchronize_imported_instances(options[:synchronize], sync_keys)
|
|
621
|
+
end
|
|
622
|
+
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
|
700
623
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
models.each_with_index do |m, i|
|
|
705
|
-
next unless m.errors.any?
|
|
624
|
+
# if we have ids, then set the id on the models and mark the models as clean.
|
|
625
|
+
if models && supports_setting_primary_key_of_imported_objects?(conn)
|
|
626
|
+
set_attributes_and_mark_clean(conn, models, return_obj, timestamps, options)
|
|
706
627
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
# create instances for each of our column/value sets
|
|
711
|
-
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
|
712
|
-
|
|
713
|
-
# keep track of the instance and the position it is currently at. if this fails
|
|
714
|
-
# validation we'll use the index to remove it from the array_of_attributes
|
|
715
|
-
arr.each_with_index do |hsh, i|
|
|
716
|
-
# utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
|
|
717
|
-
model = new do |m|
|
|
718
|
-
hsh.each_pair { |k, v| m[k] = v }
|
|
719
|
-
end
|
|
720
|
-
|
|
721
|
-
next if validator.valid_model?(model)
|
|
722
|
-
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
|
723
|
-
|
|
724
|
-
array_of_attributes[i] = nil
|
|
725
|
-
failure = model.dup
|
|
726
|
-
failure.errors.send(:initialize_dup, model.errors)
|
|
727
|
-
failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
|
|
728
|
-
end
|
|
729
|
-
array_of_attributes.compact!
|
|
628
|
+
# if there are auto-save associations on the models we imported that are new, import them as well
|
|
629
|
+
if options[:recursive]
|
|
630
|
+
import_associations_of_imported_models(models, return_obj, on_duplicate_key_update, options)
|
|
730
631
|
end
|
|
731
632
|
end
|
|
732
|
-
else
|
|
733
|
-
import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
|
734
|
-
end
|
|
735
|
-
|
|
736
|
-
if options[:synchronize]
|
|
737
|
-
sync_keys = options[:synchronize_keys] || Array(primary_key)
|
|
738
|
-
synchronize( options[:synchronize], sync_keys)
|
|
739
|
-
end
|
|
740
|
-
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
|
741
633
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
634
|
+
# apply the changes of synchronized instances that weren't marked clean above,
|
|
635
|
+
# such as when importing columns and values rather than models
|
|
636
|
+
synchronized_instances&.each { |instance| instance.changes_applied if instance.changed? }
|
|
745
637
|
|
|
746
|
-
|
|
747
|
-
if options[:recursive]
|
|
748
|
-
options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
|
|
749
|
-
import_associations(models, options.dup.merge(validate: false))
|
|
750
|
-
end
|
|
638
|
+
return_obj
|
|
751
639
|
end
|
|
752
|
-
|
|
753
|
-
return_obj
|
|
754
640
|
end
|
|
755
641
|
|
|
756
642
|
# Imports the passed in +column_names+ and +array_of_attributes+
|
|
@@ -760,7 +646,7 @@ class ActiveRecord::Base
|
|
|
760
646
|
# +num_inserts+ is the number of inserts it took to import the data. See
|
|
761
647
|
# ActiveRecord::Base.import for more information on
|
|
762
648
|
# +column_names+, +array_of_attributes+ and +options+.
|
|
763
|
-
def import_with_validations( column_names, array_of_attributes, options = {} )
|
|
649
|
+
def import_with_validations( conn, column_names, array_of_attributes, options = {} )
|
|
764
650
|
failed_instances = []
|
|
765
651
|
|
|
766
652
|
yield failed_instances if block_given?
|
|
@@ -768,9 +654,9 @@ class ActiveRecord::Base
|
|
|
768
654
|
result = if options[:all_or_none] && failed_instances.any?
|
|
769
655
|
ActiveRecord::Import::Result.new([], 0, [], [])
|
|
770
656
|
else
|
|
771
|
-
import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
|
657
|
+
import_without_validations_or_callbacks( conn, column_names, array_of_attributes, options )
|
|
772
658
|
end
|
|
773
|
-
ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results)
|
|
659
|
+
ActiveRecord::Import::Result.new(failed_instances, result.num_inserts, result.ids, result.results, result.affected_rows)
|
|
774
660
|
end
|
|
775
661
|
|
|
776
662
|
# Imports the passed in +column_names+ and +array_of_attributes+
|
|
@@ -779,12 +665,11 @@ class ActiveRecord::Base
|
|
|
779
665
|
# validations or callbacks. See ActiveRecord::Base.import for more
|
|
780
666
|
# information on +column_names+, +array_of_attributes_ and
|
|
781
667
|
# +options+.
|
|
782
|
-
def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
|
|
668
|
+
def import_without_validations_or_callbacks( conn, column_names, array_of_attributes, options = {} )
|
|
783
669
|
return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
|
|
784
670
|
|
|
785
671
|
column_names = column_names.map do |name|
|
|
786
|
-
|
|
787
|
-
original_name.to_sym
|
|
672
|
+
column_name_for_import(name).to_sym
|
|
788
673
|
end
|
|
789
674
|
scope_columns, scope_values = scope_attributes.to_a.transpose
|
|
790
675
|
|
|
@@ -803,23 +688,27 @@ class ActiveRecord::Base
|
|
|
803
688
|
end
|
|
804
689
|
|
|
805
690
|
columns = column_names.each_with_index.map do |name, i|
|
|
806
|
-
column = schema_columns_hash[name.to_s]
|
|
691
|
+
column = schema_columns_hash(conn)[name.to_s]
|
|
807
692
|
raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
|
|
808
693
|
column
|
|
809
694
|
end
|
|
810
695
|
|
|
811
|
-
columns_sql = "(#{column_names.map { |name|
|
|
812
|
-
pre_sql_statements =
|
|
696
|
+
columns_sql = "(#{column_names.map { |name| conn.quote_column_name(name) }.join(',')})"
|
|
697
|
+
pre_sql_statements = conn.pre_sql_statements( options )
|
|
813
698
|
insert_sql = ['INSERT', pre_sql_statements, "INTO #{quoted_table_name} #{columns_sql} VALUES "]
|
|
814
699
|
insert_sql = insert_sql.flatten.join(' ')
|
|
815
|
-
values_sql = values_sql_for_columns_and_attributes(columns, array_of_attributes)
|
|
700
|
+
values_sql = values_sql_for_columns_and_attributes(conn, columns, array_of_attributes, Array(options[:database_default_columns]))
|
|
701
|
+
comment_sql = sql_comment_for_annotations(conn)
|
|
816
702
|
|
|
817
703
|
number_inserted = 0
|
|
818
704
|
ids = []
|
|
819
705
|
results = []
|
|
820
|
-
|
|
706
|
+
affected_rows = 0
|
|
707
|
+
|
|
708
|
+
if supports_import?(conn)
|
|
821
709
|
# generate the sql
|
|
822
|
-
post_sql_statements =
|
|
710
|
+
post_sql_statements = conn.post_sql_statements( quoted_table_name, options )
|
|
711
|
+
post_sql_statements += [comment_sql] if comment_sql
|
|
823
712
|
import_size = values_sql.size
|
|
824
713
|
|
|
825
714
|
batch_size = options[:batch_size] || import_size
|
|
@@ -832,31 +721,269 @@ class ActiveRecord::Base
|
|
|
832
721
|
batch_started_at = Time.now.to_i
|
|
833
722
|
|
|
834
723
|
# perform the inserts
|
|
835
|
-
result =
|
|
724
|
+
result = conn.insert_many( [insert_sql, post_sql_statements].flatten,
|
|
836
725
|
batch_values,
|
|
837
726
|
options,
|
|
838
727
|
"#{model_name} Create Many" )
|
|
839
728
|
|
|
840
729
|
number_inserted += result.num_inserts
|
|
841
|
-
ids
|
|
842
|
-
results
|
|
730
|
+
ids.concat(result.ids)
|
|
731
|
+
results.concat(result.results)
|
|
732
|
+
affected_rows = result.affected_rows.nil? ? nil : (affected_rows || 0) + result.affected_rows
|
|
843
733
|
current_batch += 1
|
|
844
734
|
|
|
845
735
|
progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
|
|
846
736
|
end
|
|
847
737
|
else
|
|
738
|
+
logger&.warn "activerecord-import: the #{conn.adapter_name} adapter doesn't support bulk import, so #{model_name} " \
|
|
739
|
+
"records are being inserted one at a time. If the adapter is supported under another name, require its " \
|
|
740
|
+
"activerecord-import adapter, e.g. activerecord-import/active_record/adapters/postgresql_adapter."
|
|
741
|
+
|
|
848
742
|
transaction(requires_new: true) do
|
|
849
743
|
values_sql.each do |values|
|
|
850
|
-
ids <<
|
|
744
|
+
ids << conn.insert("#{insert_sql}#{values}#{comment_sql}", "#{model_name} Create Many")
|
|
851
745
|
number_inserted += 1
|
|
852
746
|
end
|
|
853
747
|
end
|
|
854
748
|
end
|
|
855
|
-
ActiveRecord::Import::Result.new([], number_inserted, ids, results)
|
|
749
|
+
ActiveRecord::Import::Result.new([], number_inserted, ids, results, affected_rows)
|
|
856
750
|
end
|
|
857
751
|
|
|
858
752
|
private
|
|
859
753
|
|
|
754
|
+
# Takes the options Hash, if any, off the end of +args+ and returns the options
|
|
755
|
+
# for the import, with their defaults, along with the arguments to import.
|
|
756
|
+
def options_and_arguments_for_import(args)
|
|
757
|
+
options = { validate: true, timestamps: true, track_validation_failures: false }
|
|
758
|
+
options.merge!( args.pop ) if args.last.is_a? Hash
|
|
759
|
+
args = arrays_from_array_like_import_arguments(args)
|
|
760
|
+
# making sure that the current model and its primary key are used, rather than
|
|
761
|
+
# the parent model's when importing associations recursively
|
|
762
|
+
options[:model] = self
|
|
763
|
+
options[:primary_key] = primary_key
|
|
764
|
+
options[:locking_column] = locking_column if locking_enabled?
|
|
765
|
+
[options, args]
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Returns the models to import from +args+, the column names to import them with,
|
|
769
|
+
# and the columns the database populates for the models that don't assign them,
|
|
770
|
+
# which is nil when +args+ give the column names.
|
|
771
|
+
def models_and_column_names_for_import(conn, args)
|
|
772
|
+
if args.length == 2
|
|
773
|
+
models = args.last
|
|
774
|
+
column_names = args.first.dup
|
|
775
|
+
else
|
|
776
|
+
models = args.first
|
|
777
|
+
column_names = if conn.try(:supports_virtual_columns?)
|
|
778
|
+
columns.reject(&:virtual?).map(&:name)
|
|
779
|
+
else
|
|
780
|
+
self.column_names.dup
|
|
781
|
+
end
|
|
782
|
+
populated_column_names = database_populated_column_names(conn, column_names, models)
|
|
783
|
+
# Leave out the ones no model assigns, and let the database fill in the rest for the models that don't
|
|
784
|
+
default_column_names = populated_column_names.select { |name| models.any? { |model| model.attribute_changed?(name) } }
|
|
785
|
+
column_names -= populated_column_names - default_column_names
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Leave out a primary key the database populates, like a uuid, only when no model has one, so
|
|
789
|
+
# assigned ids are kept. Otherwise the database fills it in for the models without one.
|
|
790
|
+
column_names -= database_populated_primary_keys(conn) if models.all? { |model| model.id.nil? }
|
|
791
|
+
|
|
792
|
+
[models, column_names, default_column_names]
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Returns the values of +column_names+ for each of +models+, the rows to import. Models
|
|
796
|
+
# that +validator+ finds invalid are left out, or raise ActiveRecord::RecordInvalid with
|
|
797
|
+
# the :raise_error option. Without a +validator+, the models aren't validated.
|
|
798
|
+
def attributes_from_models(conn, models, column_names, validator, options)
|
|
799
|
+
update_attrs = if record_timestamps && options[:timestamps]
|
|
800
|
+
send(:timestamp_attributes_for_update).map(&:to_sym)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
array_of_attributes = []
|
|
804
|
+
|
|
805
|
+
models.each do |model|
|
|
806
|
+
if supports_setting_primary_key_of_imported_objects?(conn)
|
|
807
|
+
load_association_ids(model)
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
if validator && !validator.valid_model?(model)
|
|
811
|
+
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
|
812
|
+
next
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
array_of_attributes << column_names.map do |name|
|
|
816
|
+
if model.persisted? &&
|
|
817
|
+
update_attrs && update_attrs.include?(name.to_sym) &&
|
|
818
|
+
!model.send("#{name}_changed?")
|
|
819
|
+
nil
|
|
820
|
+
else
|
|
821
|
+
name = name.to_s
|
|
822
|
+
# Read a column's own value, as saving the model would, even if it's also an alias
|
|
823
|
+
value = columns_hash.key?(name) ? model._read_attribute(name) : model.read_attribute(name)
|
|
824
|
+
model.instance_of?(self) ? value : value_from_model_type(model, name, value)
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
array_of_attributes
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
# Returns the column names and the rows to import from +args+, which are either an
|
|
833
|
+
# array of hashes, optionally after column names, or column names and an array of values.
|
|
834
|
+
def column_names_and_attributes_for_import(args)
|
|
835
|
+
# supports array of hash objects
|
|
836
|
+
if args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
|
|
837
|
+
if args.length == 2
|
|
838
|
+
array_of_hashes = args.last
|
|
839
|
+
column_names = args.first.dup
|
|
840
|
+
allow_extra_hash_keys = true
|
|
841
|
+
else
|
|
842
|
+
array_of_hashes = args.first
|
|
843
|
+
column_names = array_of_hashes.first.keys
|
|
844
|
+
allow_extra_hash_keys = false
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
array_of_attributes = array_of_hashes.map do |h|
|
|
848
|
+
error_message = validate_hash_import(h, column_names, allow_extra_hash_keys)
|
|
849
|
+
|
|
850
|
+
raise ArgumentError, error_message if error_message
|
|
851
|
+
|
|
852
|
+
column_names.map do |key|
|
|
853
|
+
h[key]
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
# supports 2-element array and array
|
|
857
|
+
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
|
858
|
+
|
|
859
|
+
unless args.last.first.is_a?(Array)
|
|
860
|
+
raise ArgumentError, "Last argument should be a two dimensional array '[[]]'. First element in array was a #{args.last.first.class}"
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
column_names, array_of_attributes = args
|
|
864
|
+
|
|
865
|
+
# dup the passed args so we don't modify unintentionally
|
|
866
|
+
column_names = column_names.dup
|
|
867
|
+
array_of_attributes = array_of_attributes.map(&:dup)
|
|
868
|
+
else
|
|
869
|
+
raise ArgumentError, invalid_import_arguments_message(args)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
[column_names, array_of_attributes]
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
# Returns a copy of the +on_duplicate_key_update+ option to import with, so it can be
|
|
876
|
+
# changed, such as when adding timestamp columns, without modifying the option that was
|
|
877
|
+
# passed in. :all is replaced by the columns other than the primary key and the
|
|
878
|
+
# timestamps recorded on create.
|
|
879
|
+
def on_duplicate_key_update_for_import(on_duplicate_key_update, column_names, options)
|
|
880
|
+
symbolized_column_names = Array(column_names).map(&:to_sym)
|
|
881
|
+
symbolized_primary_key = Array(primary_key).map(&:to_sym)
|
|
882
|
+
|
|
883
|
+
updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
|
|
884
|
+
# Like the primary key, timestamps recorded on create shouldn't change when a record is updated
|
|
885
|
+
if record_timestamps && options[:timestamps]
|
|
886
|
+
updatable_columns -= timestamp_attributes_for_create_in_model.map(&:to_sym)
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
if on_duplicate_key_update.is_a?(Hash)
|
|
890
|
+
on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
|
|
891
|
+
duped_options[k] = if k == :columns && v == :all
|
|
892
|
+
updatable_columns
|
|
893
|
+
elsif v.duplicable?
|
|
894
|
+
v.dup
|
|
895
|
+
else
|
|
896
|
+
v
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
elsif on_duplicate_key_update == :all
|
|
900
|
+
updatable_columns
|
|
901
|
+
elsif on_duplicate_key_update.duplicable?
|
|
902
|
+
on_duplicate_key_update.dup
|
|
903
|
+
else
|
|
904
|
+
on_duplicate_key_update
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# Force the primary key col into the insert if it's not
|
|
909
|
+
# on the list and we are using a sequence and stuff a nil
|
|
910
|
+
# value for it into each row so the sequencer will fire later
|
|
911
|
+
def add_primary_key_for_sequence(conn, column_names, array_of_attributes)
|
|
912
|
+
symbolized_column_names = Array(column_names).map(&:to_sym)
|
|
913
|
+
symbolized_primary_key = Array(primary_key).map(&:to_sym)
|
|
914
|
+
|
|
915
|
+
if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && conn.prefetch_primary_key? && sequence_name
|
|
916
|
+
column_count = column_names.size
|
|
917
|
+
column_names.concat(Array(primary_key)).uniq!
|
|
918
|
+
columns_added = column_names.size - column_count
|
|
919
|
+
new_fields = Array.new(columns_added)
|
|
920
|
+
array_of_attributes.each { |a| a.concat(new_fields) }
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# Adds the models that failed validation, when their rows were read, to +failed_instances+.
|
|
925
|
+
def add_failed_models(failed_instances, models, options)
|
|
926
|
+
models.each_with_index do |m, i|
|
|
927
|
+
next unless m.errors.any?
|
|
928
|
+
|
|
929
|
+
failed_instances << (options[:track_validation_failures] ? [i, m] : m)
|
|
930
|
+
end
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
# Validates each row of +array_of_attributes+ as a new model, adding the models that
|
|
934
|
+
# fail to +failed_instances+ and removing their rows from +array_of_attributes+.
|
|
935
|
+
def validate_attributes_for_import(failed_instances, column_names, array_of_attributes, validator, options)
|
|
936
|
+
# create instances for each of our column/value sets
|
|
937
|
+
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
|
938
|
+
|
|
939
|
+
# keep track of the instance and the position it is currently at. if this fails
|
|
940
|
+
# validation we'll use the index to remove it from the array_of_attributes
|
|
941
|
+
arr.each_with_index do |hsh, i|
|
|
942
|
+
# utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
|
|
943
|
+
model = new do |m|
|
|
944
|
+
hsh.each_pair do |k, v|
|
|
945
|
+
if write_id_column_directly?(k)
|
|
946
|
+
m._write_attribute("id", v)
|
|
947
|
+
else
|
|
948
|
+
m[k] = v
|
|
949
|
+
end
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
next if validator.valid_model?(model)
|
|
954
|
+
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
|
955
|
+
|
|
956
|
+
array_of_attributes[i] = nil
|
|
957
|
+
failure = model.dup
|
|
958
|
+
failure.errors.send(:initialize_dup, model.errors)
|
|
959
|
+
failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
|
|
960
|
+
end
|
|
961
|
+
array_of_attributes.compact!
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
# Imports the new and changed associations of the +models+ that were imported, with the
|
|
965
|
+
# :on_duplicate_key_update option that was passed in rather than the copy used for +models+.
|
|
966
|
+
def import_associations_of_imported_models(models, import_result, on_duplicate_key_update, options)
|
|
967
|
+
options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
|
|
968
|
+
import_associations(imported_models(models, import_result, options), options.dup.merge(validate: false))
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
# ActiveRecord treats "id" as the primary key when writing attributes
|
|
972
|
+
# through id= or []=. When the primary key is composite or is another
|
|
973
|
+
# column, an "id" column has to be written directly instead.
|
|
974
|
+
def write_id_column_directly?(column)
|
|
975
|
+
column.to_s == "id" && primary_key != "id"
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Returns SQL comments for the annotations on the current scope, as
|
|
979
|
+
# ActiveRecord adds them to its queries, or nil if there are none.
|
|
980
|
+
def sql_comment_for_annotations(conn)
|
|
981
|
+
annotations = all.annotate_values
|
|
982
|
+
return if annotations.empty?
|
|
983
|
+
|
|
984
|
+
" #{annotations.map { |value| "/* #{conn.sanitize_as_sql_comment(value)} */" }.join(' ')}"
|
|
985
|
+
end
|
|
986
|
+
|
|
860
987
|
def associated_options(options, association)
|
|
861
988
|
return options unless options.key?(:recursive_on_duplicate_key_update)
|
|
862
989
|
|
|
@@ -865,9 +992,55 @@ class ActiveRecord::Base
|
|
|
865
992
|
)
|
|
866
993
|
end
|
|
867
994
|
|
|
868
|
-
|
|
995
|
+
# Returns the models that weren't rejected by validations.
|
|
996
|
+
def imported_models(models, import_result, options)
|
|
997
|
+
failed_instances = import_result.failed_instances
|
|
998
|
+
failed_instances = failed_instances.map(&:last) if options[:track_validation_failures]
|
|
999
|
+
models - failed_instances
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
# Synchronizes +instances+ with the database like ActiveRecord::Base.synchronize,
|
|
1003
|
+
# but rather than discarding their changes, it assigns the database values
|
|
1004
|
+
# on top of them. Once the instances are marked clean, previous_changes then
|
|
1005
|
+
# reflects what the import changed, as it does without synchronizing.
|
|
1006
|
+
# Instances left without changes are marked clean right away. Returns the
|
|
1007
|
+
# instances that were synchronized.
|
|
1008
|
+
def synchronize_imported_instances(instances, keys)
|
|
1009
|
+
synchronize_instances(instances, keys) do |instance, fresh_attributes|
|
|
1010
|
+
attributes = instance.instance_variable_get(:@attributes)
|
|
1011
|
+
# the attributes of destroyed records are frozen
|
|
1012
|
+
attributes = attributes.deep_dup if attributes.frozen?
|
|
1013
|
+
|
|
1014
|
+
names = fresh_attributes.keys
|
|
1015
|
+
if names.all? { |name| attributes.key?(name) }
|
|
1016
|
+
names.each do |name|
|
|
1017
|
+
value = fresh_attributes.fetch_value(name)
|
|
1018
|
+
attributes.write_from_user(name, value) unless same_database_value?(attributes.fetch_value(name), value)
|
|
1019
|
+
end
|
|
1020
|
+
instance.instance_variable_set :@attributes, attributes
|
|
1021
|
+
instance.changes_applied unless instance.changed?
|
|
1022
|
+
else
|
|
1023
|
+
# the instance doesn't have all of the attributes, such as when it was
|
|
1024
|
+
# loaded with select, so take the record's attributes as they are
|
|
1025
|
+
instance.instance_variable_set :@attributes, fresh_attributes
|
|
1026
|
+
instance.clear_changes_information
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
# Returns true if +database_value+, reloaded from the database, is the +value+ an instance
|
|
1032
|
+
# already has. Databases store times to the microsecond, so a time with more precision,
|
|
1033
|
+
# which ActiveRecord < 7.0 keeps in memory, comes back rounded or truncated.
|
|
1034
|
+
def same_database_value?(value, database_value)
|
|
1035
|
+
return true if value == database_value
|
|
1036
|
+
return false unless value.is_a?(Time) && database_value.is_a?(Time)
|
|
1037
|
+
|
|
1038
|
+
value.round(6) == database_value || value.floor(6) == database_value
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def set_attributes_and_mark_clean(conn, models, import_result, timestamps, options)
|
|
869
1042
|
return if models.nil?
|
|
870
|
-
models
|
|
1043
|
+
models = imported_models(models, import_result, options)
|
|
871
1044
|
|
|
872
1045
|
# if ids were returned for all models we know all were updated
|
|
873
1046
|
if models.size == import_result.ids.size
|
|
@@ -882,22 +1055,20 @@ class ActiveRecord::Base
|
|
|
882
1055
|
end
|
|
883
1056
|
|
|
884
1057
|
deserialize_value = lambda do |column, value|
|
|
885
|
-
column = schema_columns_hash[column]
|
|
1058
|
+
column = schema_columns_hash(conn)[column]
|
|
886
1059
|
return value unless column
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
type.deserialize(value)
|
|
890
|
-
elsif column.respond_to?(:type_cast_from_database)
|
|
891
|
-
column.type_cast_from_database(value)
|
|
892
|
-
else
|
|
893
|
-
value
|
|
894
|
-
end
|
|
1060
|
+
type = type_for_attribute(column.name)
|
|
1061
|
+
type.deserialize(value)
|
|
895
1062
|
end
|
|
896
1063
|
|
|
897
1064
|
set_value = lambda do |model, column, value|
|
|
898
1065
|
val = deserialize_value.call(column, value)
|
|
899
1066
|
if model.attribute_names.include?(column)
|
|
900
|
-
|
|
1067
|
+
if write_id_column_directly?(column)
|
|
1068
|
+
model._write_attribute(column, val)
|
|
1069
|
+
else
|
|
1070
|
+
model.send("#{column}=", val)
|
|
1071
|
+
end
|
|
901
1072
|
else
|
|
902
1073
|
attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
|
|
903
1074
|
model.instance_variable_set(:@attributes, attributes)
|
|
@@ -922,14 +1093,7 @@ class ActiveRecord::Base
|
|
|
922
1093
|
end
|
|
923
1094
|
|
|
924
1095
|
models.each do |model|
|
|
925
|
-
|
|
926
|
-
model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
|
|
927
|
-
model.changes_applied
|
|
928
|
-
elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
|
|
929
|
-
model.clear_changes_information
|
|
930
|
-
else # Rails 3.2
|
|
931
|
-
model.instance_variable_get(:@changed_attributes).clear
|
|
932
|
-
end
|
|
1096
|
+
model.changes_applied
|
|
933
1097
|
model.instance_variable_set(:@new_record, false)
|
|
934
1098
|
end
|
|
935
1099
|
end
|
|
@@ -949,12 +1113,24 @@ class ActiveRecord::Base
|
|
|
949
1113
|
association = association.target
|
|
950
1114
|
next if association.blank? || model.public_send(column_name).present?
|
|
951
1115
|
|
|
952
|
-
association_primary_key =
|
|
1116
|
+
association_primary_key = association_primary_key_names(association_reflection)[column_index]
|
|
1117
|
+
next unless association_primary_key
|
|
1118
|
+
|
|
953
1119
|
model.public_send("#{column_name}=", association.send(association_primary_key))
|
|
954
1120
|
end
|
|
955
1121
|
end
|
|
956
1122
|
end
|
|
957
1123
|
|
|
1124
|
+
# Returns the names of the columns that a belongs_to association's foreign key refers to.
|
|
1125
|
+
# For a composite key, ActiveRecord 7.1+ returns an Array, while older versions return
|
|
1126
|
+
# its string form, e.g. "[:account_id, :id]".
|
|
1127
|
+
def association_primary_key_names(association_reflection)
|
|
1128
|
+
association_primary_key = association_reflection.association_primary_key
|
|
1129
|
+
return association_primary_key.map(&:to_s) if association_primary_key.is_a?(Array)
|
|
1130
|
+
|
|
1131
|
+
association_primary_key.tr("[]:", "").split(", ")
|
|
1132
|
+
end
|
|
1133
|
+
|
|
958
1134
|
def import_associations(models, options)
|
|
959
1135
|
# now, for all the dirty associations, collect them into a new set of models, then recurse.
|
|
960
1136
|
# notes:
|
|
@@ -980,9 +1156,46 @@ class ActiveRecord::Base
|
|
|
980
1156
|
end
|
|
981
1157
|
end
|
|
982
1158
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1159
|
+
# Returns the columns the database populates itself, such as with a default
|
|
1160
|
+
# function, when importing new +models+. Like ActiveRecord's own inserts, the
|
|
1161
|
+
# database populates these for models that don't assign a value, instead of
|
|
1162
|
+
# inserting NULL. The primary key is handled separately.
|
|
1163
|
+
def database_populated_column_names(conn, column_names, models)
|
|
1164
|
+
return [] if models.any?(&:persisted?)
|
|
1165
|
+
|
|
1166
|
+
columns_hash = schema_columns_hash(conn)
|
|
1167
|
+
primary_keys = Array(primary_key).map(&:to_s)
|
|
1168
|
+
|
|
1169
|
+
column_names.select do |name|
|
|
1170
|
+
column = columns_hash[name.to_s]
|
|
1171
|
+
next false if column.nil? || primary_keys.include?(name.to_s)
|
|
1172
|
+
|
|
1173
|
+
column.respond_to?(:auto_populated?) ? column.auto_populated? : column.default_function
|
|
1174
|
+
end
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
# Returns the primary key columns the database populates itself, such as a uuid
|
|
1178
|
+
# or a key with a default function, rather than from the model's sequence.
|
|
1179
|
+
def database_populated_primary_keys(conn)
|
|
1180
|
+
Array(primary_key).select do |name|
|
|
1181
|
+
column = schema_columns_hash(conn)[name]
|
|
1182
|
+
column && (column.type == :uuid || (column.default_function && sequence_name.blank?))
|
|
1183
|
+
end
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
# A model of another class, such as an STI subclass with its own enum, can give
|
|
1187
|
+
# a column a different type than this class, which serializes the imported values.
|
|
1188
|
+
# Convert the model's value to this class's type through its database value.
|
|
1189
|
+
def value_from_model_type(model, name, value)
|
|
1190
|
+
name = column_name_for_import(name)
|
|
1191
|
+
model_type = model.class.type_for_attribute(name)
|
|
1192
|
+
type = type_for_attribute(name)
|
|
1193
|
+
model_type.equal?(type) ? value : type.deserialize(model_type.serialize(value))
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
def schema_columns_hash(conn)
|
|
1197
|
+
if ignored_columns.any?
|
|
1198
|
+
conn.schema_cache.columns_hash(table_name)
|
|
986
1199
|
else
|
|
987
1200
|
columns_hash
|
|
988
1201
|
end
|
|
@@ -1006,18 +1219,25 @@ class ActiveRecord::Base
|
|
|
1006
1219
|
# Wrap target in an array if not already
|
|
1007
1220
|
association = Array(association.target)
|
|
1008
1221
|
|
|
1009
|
-
|
|
1222
|
+
foreign_key_columns = Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key)
|
|
1223
|
+
foreign_key_values = association_foreign_key_values(model, association_reflection)
|
|
1224
|
+
|
|
1225
|
+
# For polymorphic associations
|
|
1226
|
+
association_name = model.class.polymorphic_name
|
|
1227
|
+
|
|
1228
|
+
# Import new and changed records, and existing records that were assigned to
|
|
1229
|
+
# this model but aren't linked to it yet, as saving the model would
|
|
1230
|
+
changed_objects = association.select do |child|
|
|
1231
|
+
child.new_record? || child.changed? ||
|
|
1232
|
+
(!association_reflection.through_reflection? &&
|
|
1233
|
+
reassigned_to_parent?(child, foreign_key_columns, foreign_key_values, association_reflection.type, association_name))
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1010
1236
|
changed_objects.each do |child|
|
|
1011
|
-
|
|
1012
|
-
child.public_send("#{column}=",
|
|
1237
|
+
foreign_key_columns.each_with_index do |column, index|
|
|
1238
|
+
child.public_send("#{column}=", foreign_key_values[index])
|
|
1013
1239
|
end
|
|
1014
1240
|
|
|
1015
|
-
# For polymorphic associations
|
|
1016
|
-
association_name = if model.class.respond_to?(:polymorphic_name)
|
|
1017
|
-
model.class.polymorphic_name
|
|
1018
|
-
else
|
|
1019
|
-
model.class.base_class
|
|
1020
|
-
end
|
|
1021
1241
|
association_reflection.type.try do |type|
|
|
1022
1242
|
child.public_send("#{type}=", association_name)
|
|
1023
1243
|
end
|
|
@@ -1027,37 +1247,55 @@ class ActiveRecord::Base
|
|
|
1027
1247
|
associated_objects_by_class
|
|
1028
1248
|
end
|
|
1029
1249
|
|
|
1030
|
-
# Returns
|
|
1031
|
-
#
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1250
|
+
# Returns the values of +model+ that the foreign key of +association_reflection+
|
|
1251
|
+
# refers to: the columns named by the association's :primary_key option when
|
|
1252
|
+
# one is given (e.g. <tt>has_many :reviews, primary_key: :identifier</tt>),
|
|
1253
|
+
# and the model's id otherwise.
|
|
1254
|
+
def association_foreign_key_values(model, association_reflection)
|
|
1255
|
+
if association_reflection.options[:primary_key]
|
|
1256
|
+
Array(association_reflection.active_record_primary_key).map { |key| model.read_attribute(key) }
|
|
1257
|
+
else
|
|
1258
|
+
Array(model.id)
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
# Returns true if +child+, an existing record of a has_one or has_many association,
|
|
1263
|
+
# doesn't refer to the parent yet, such as when it was assigned to a new parent.
|
|
1264
|
+
def reassigned_to_parent?(child, foreign_key_columns, foreign_key_values, type_column, type_name)
|
|
1265
|
+
return false if child.new_record?
|
|
1036
1266
|
|
|
1267
|
+
parent_values = foreign_key_columns.zip(foreign_key_values)
|
|
1268
|
+
parent_values << [type_column, type_name.to_s] if type_column.present?
|
|
1269
|
+
parent_values.any? { |column, value| child.has_attribute?(column) && child.read_attribute(column) != value }
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
# Returns SQL the VALUES for an INSERT statement given the passed in +columns+
|
|
1273
|
+
# and +array_of_attributes+. Nil values for +default_column_names+ use the
|
|
1274
|
+
# database's default.
|
|
1275
|
+
def values_sql_for_columns_and_attributes(conn, columns, array_of_attributes, default_column_names = []) # :nodoc:
|
|
1037
1276
|
array_of_attributes.map do |arr|
|
|
1277
|
+
if arr.length < columns.length
|
|
1278
|
+
raise ArgumentError, "Number of values (#{arr.length}) is less than number of columns (#{columns.length})"
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1038
1281
|
my_values = arr.each_with_index.map do |val, j|
|
|
1039
1282
|
column = columns[j]
|
|
1040
1283
|
|
|
1041
1284
|
# be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
|
|
1042
1285
|
if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
|
|
1043
|
-
|
|
1286
|
+
conn.next_value_for_sequence(sequence_name)
|
|
1287
|
+
elsif val.nil? && default_column_names.include?(column.name)
|
|
1288
|
+
unless conn.supports_default_keyword_in_values?
|
|
1289
|
+
raise ArgumentError, "#{conn.adapter_name} doesn't support DEFAULT in VALUES, so the database can't populate " \
|
|
1290
|
+
"#{table_name}.#{column.name} for only the rows without a value. Import those rows separately so the column can be left out."
|
|
1291
|
+
end
|
|
1292
|
+
"DEFAULT"
|
|
1044
1293
|
elsif val.respond_to?(:to_sql)
|
|
1045
1294
|
"(#{val.to_sql})"
|
|
1046
1295
|
elsif column
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
connection_memo.quote(val)
|
|
1051
|
-
elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
|
|
1052
|
-
connection_memo.quote(column.type_cast_from_user(val), column)
|
|
1053
|
-
else # Rails 3.2, 4.0 and 4.1
|
|
1054
|
-
if serialized_attributes.include?(column.name)
|
|
1055
|
-
val = serialized_attributes[column.name].dump(val)
|
|
1056
|
-
end
|
|
1057
|
-
# Fixes #443 to support binary (i.e. bytea) columns on PG
|
|
1058
|
-
val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
|
|
1059
|
-
connection_memo.quote(val, column)
|
|
1060
|
-
end
|
|
1296
|
+
type = type_for_attribute(column.name)
|
|
1297
|
+
val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
|
|
1298
|
+
conn.quote(val)
|
|
1061
1299
|
else
|
|
1062
1300
|
raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
|
|
1063
1301
|
end
|
|
@@ -1066,18 +1304,12 @@ class ActiveRecord::Base
|
|
|
1066
1304
|
end
|
|
1067
1305
|
end
|
|
1068
1306
|
|
|
1069
|
-
def add_special_rails_stamps( column_names, array_of_attributes, options )
|
|
1307
|
+
def add_special_rails_stamps( conn, column_names, array_of_attributes, options )
|
|
1070
1308
|
timestamp_columns = {}
|
|
1071
1309
|
timestamps = {}
|
|
1072
1310
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
|
1076
|
-
else
|
|
1077
|
-
instance = allocate
|
|
1078
|
-
timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
|
|
1079
|
-
timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
|
|
1080
|
-
end
|
|
1311
|
+
timestamp_columns[:create] = timestamp_attributes_for_create_in_model
|
|
1312
|
+
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
|
1081
1313
|
|
|
1082
1314
|
# use tz as set in ActiveRecord::Base
|
|
1083
1315
|
default_timezone = if ActiveRecord.respond_to?(:default_timezone)
|
|
@@ -1101,8 +1333,8 @@ class ActiveRecord::Base
|
|
|
1101
1333
|
array_of_attributes.each { |arr| arr << timestamp }
|
|
1102
1334
|
end
|
|
1103
1335
|
|
|
1104
|
-
if supports_on_duplicate_key_update? && action == :update
|
|
1105
|
-
|
|
1336
|
+
if supports_on_duplicate_key_update?(conn) && action == :update
|
|
1337
|
+
conn.add_column_for_on_duplicate_key_update(column, options)
|
|
1106
1338
|
end
|
|
1107
1339
|
end
|
|
1108
1340
|
end
|
|
@@ -1110,6 +1342,17 @@ class ActiveRecord::Base
|
|
|
1110
1342
|
timestamps
|
|
1111
1343
|
end
|
|
1112
1344
|
|
|
1345
|
+
# Converts array-like arguments, such as a Relation or a Set, to arrays.
|
|
1346
|
+
def arrays_from_array_like_import_arguments(args) # :nodoc:
|
|
1347
|
+
args.map { |arg| arg.is_a?(Enumerable) && !arg.is_a?(Array) && !arg.is_a?(Hash) ? arg.to_a : arg }
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def invalid_import_arguments_message(args) # :nodoc:
|
|
1351
|
+
received = args.map { |arg| arg.is_a?(Array) && arg.any? ? "Array of #{arg.first.class}" : arg.class.name }
|
|
1352
|
+
"Invalid arguments! Expected an array of models, an array of hashes, or column names and an array " \
|
|
1353
|
+
"of values, but received #{received.empty? ? 'no arguments' : received.join(', ')}"
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1113
1356
|
# Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
|
|
1114
1357
|
def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
|
|
1115
1358
|
array_of_attributes.map { |values| Hash[column_names.zip(values)] }
|