activerecord-import 2.3.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 +91 -0
- data/README.markdown +91 -24
- data/lib/activerecord-import/adapters/abstract_adapter.rb +58 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +69 -19
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +9 -51
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -3
- data/lib/activerecord-import/aliases.rb +42 -0
- data/lib/activerecord-import/base.rb +2 -44
- data/lib/activerecord-import/core.rb +62 -0
- data/lib/activerecord-import/import.rb +534 -327
- 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 +7 -4
|
@@ -23,126 +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
|
-
callback.instance_variable_set(:@compiled, nil)
|
|
64
|
-
chain[i] = callback
|
|
65
|
-
end
|
|
66
|
-
else
|
|
67
|
-
callback.raw_filter = filter
|
|
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)
|
|
72
61
|
end
|
|
73
62
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
validation_context ||= (model.new_record? ? :create : :update)
|
|
79
|
-
current_context = model.send(:validation_context)
|
|
80
|
-
|
|
81
|
-
begin
|
|
82
|
-
set_validation_context(model, validation_context)
|
|
83
|
-
model.errors.clear
|
|
84
|
-
|
|
85
|
-
model.run_callbacks(:validation) do
|
|
86
|
-
if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
|
|
87
|
-
runner = if @validate_callbacks.method(:compile).arity == 0
|
|
88
|
-
@validate_callbacks.compile
|
|
89
|
-
else # ActiveRecord >= 7.1
|
|
90
|
-
@validate_callbacks.compile(nil)
|
|
91
|
-
end
|
|
92
|
-
env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
|
|
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
|
|
116
|
-
end
|
|
63
|
+
def initialize(options = {})
|
|
64
|
+
@options = options
|
|
65
|
+
@foreign_key_presence_validators = {}
|
|
66
|
+
end
|
|
117
67
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
68
|
+
def valid_model?(model)
|
|
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
|
|
122
74
|
end
|
|
123
75
|
|
|
124
|
-
|
|
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)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
125
89
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# validation context object reachable through the private
|
|
129
|
-
# +context_for_validation+ reader. Support both so that validated imports
|
|
130
|
-
# keep working across all supported ActiveRecord versions.
|
|
131
|
-
def set_validation_context(model, context)
|
|
132
|
-
if model.respond_to?(:validation_context=, true)
|
|
133
|
-
model.send(:validation_context=, context)
|
|
134
|
-
else
|
|
135
|
-
model.send(:context_for_validation).context = 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) }
|
|
136
92
|
end
|
|
137
93
|
end
|
|
138
94
|
end
|
|
139
95
|
end
|
|
140
96
|
|
|
97
|
+
ActiveRecord::Validations::UniquenessValidator.prepend(ActiveRecord::Import::SkipUniquenessValidation)
|
|
98
|
+
ActiveModel::Validations::PresenceValidator.prepend(ActiveRecord::Import::ForeignKeyPresenceValidation)
|
|
99
|
+
|
|
141
100
|
class ActiveRecord::Associations::CollectionProxy
|
|
142
101
|
def bulk_import(*args, &block)
|
|
143
102
|
@association.bulk_import(*args, &block)
|
|
144
103
|
end
|
|
145
|
-
alias import bulk_import unless respond_to? :import
|
|
146
104
|
end
|
|
147
105
|
|
|
148
106
|
class ActiveRecord::Associations::CollectionAssociation
|
|
@@ -154,9 +112,10 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
154
112
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
155
113
|
|
|
156
114
|
model_klass = reflection.klass
|
|
115
|
+
args = model_klass.send(:arrays_from_array_like_import_arguments, args)
|
|
157
116
|
symbolized_foreign_key = reflection.foreign_key.to_sym
|
|
158
117
|
|
|
159
|
-
symbolized_column_names = if model_klass.connection_pool.with_connection
|
|
118
|
+
symbolized_column_names = if model_klass.connection_pool.with_connection(&:supports_virtual_columns?)
|
|
160
119
|
model_klass.columns.reject(&:virtual?).map { |c| c.name.to_sym }
|
|
161
120
|
else
|
|
162
121
|
model_klass.column_names.map(&:to_sym)
|
|
@@ -164,12 +123,14 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
164
123
|
|
|
165
124
|
owner_primary_key = reflection.active_record_primary_key.to_sym
|
|
166
125
|
owner_primary_key_value = owner.send(owner_primary_key)
|
|
126
|
+
owner_polymorphic_name = owner.class.polymorphic_name if reflection.type
|
|
167
127
|
|
|
168
128
|
# assume array of model objects
|
|
169
129
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
|
170
130
|
if args.length == 2
|
|
171
131
|
models = args.last
|
|
172
132
|
column_names = args.first.dup
|
|
133
|
+
symbolized_column_names = column_names.map(&:to_sym)
|
|
173
134
|
else
|
|
174
135
|
models = args.first
|
|
175
136
|
column_names = symbolized_column_names
|
|
@@ -179,9 +140,13 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
179
140
|
column_names << symbolized_foreign_key
|
|
180
141
|
end
|
|
181
142
|
|
|
143
|
+
if reflection.type && !symbolized_column_names.include?(reflection.type.to_sym)
|
|
144
|
+
column_names << reflection.type.to_sym
|
|
145
|
+
end
|
|
146
|
+
|
|
182
147
|
models.each do |m|
|
|
183
148
|
m.public_send "#{symbolized_foreign_key}=", owner_primary_key_value
|
|
184
|
-
m.public_send "#{reflection.type}=",
|
|
149
|
+
m.public_send "#{reflection.type}=", owner_polymorphic_name if reflection.type
|
|
185
150
|
end
|
|
186
151
|
|
|
187
152
|
model_klass.bulk_import column_names, models, options
|
|
@@ -198,6 +163,7 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
198
163
|
allow_extra_hash_keys = false
|
|
199
164
|
end
|
|
200
165
|
|
|
166
|
+
required_column_names = column_names.dup
|
|
201
167
|
symbolized_column_names = column_names.map(&:to_sym)
|
|
202
168
|
unless symbolized_column_names.include?(symbolized_foreign_key)
|
|
203
169
|
column_names << symbolized_foreign_key
|
|
@@ -208,15 +174,16 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
208
174
|
end
|
|
209
175
|
|
|
210
176
|
array_of_attributes = array_of_hashes.map do |h|
|
|
211
|
-
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)
|
|
212
178
|
|
|
213
179
|
raise ArgumentError, error_message if error_message
|
|
214
180
|
|
|
215
181
|
column_names.map do |key|
|
|
216
|
-
|
|
182
|
+
symbolized_key = key.to_sym
|
|
183
|
+
if symbolized_key == symbolized_foreign_key
|
|
217
184
|
owner_primary_key_value
|
|
218
|
-
elsif reflection.type &&
|
|
219
|
-
|
|
185
|
+
elsif reflection.type && symbolized_key == reflection.type.to_sym
|
|
186
|
+
owner_polymorphic_name
|
|
220
187
|
else
|
|
221
188
|
h[key]
|
|
222
189
|
end
|
|
@@ -251,19 +218,18 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
251
218
|
symbolized_type = reflection.type.to_sym
|
|
252
219
|
if symbolized_column_names.include?(symbolized_type)
|
|
253
220
|
index = symbolized_column_names.index(symbolized_type)
|
|
254
|
-
array_of_attributes.each { |attrs| attrs[index] =
|
|
221
|
+
array_of_attributes.each { |attrs| attrs[index] = owner_polymorphic_name }
|
|
255
222
|
else
|
|
256
223
|
column_names << symbolized_type
|
|
257
|
-
array_of_attributes.each { |attrs| attrs <<
|
|
224
|
+
array_of_attributes.each { |attrs| attrs << owner_polymorphic_name }
|
|
258
225
|
end
|
|
259
226
|
end
|
|
260
227
|
|
|
261
228
|
model_klass.bulk_import column_names, array_of_attributes, options
|
|
262
229
|
else
|
|
263
|
-
raise ArgumentError,
|
|
230
|
+
raise ArgumentError, model_klass.send(:invalid_import_arguments_message, args)
|
|
264
231
|
end
|
|
265
232
|
end
|
|
266
|
-
alias import bulk_import unless respond_to? :import
|
|
267
233
|
end
|
|
268
234
|
|
|
269
235
|
module ActiveRecord::Import::ConnectionHandler
|
|
@@ -383,7 +349,7 @@ class ActiveRecord::Base
|
|
|
383
349
|
# (if false) even if record timestamps is disabled in ActiveRecord::Base
|
|
384
350
|
# * +recursive+ - true|false, tells import to import all has_many/has_one
|
|
385
351
|
# associations if the adapter supports setting the primary keys of the
|
|
386
|
-
# newly imported objects. PostgreSQL only.
|
|
352
|
+
# newly imported objects. PostgreSQL and MariaDB 10.5+ only.
|
|
387
353
|
# * +batch_size+ - an integer value to specify the max number of records to
|
|
388
354
|
# include per insert. Defaults to the total number of records to import.
|
|
389
355
|
#
|
|
@@ -438,7 +404,8 @@ class ActiveRecord::Base
|
|
|
438
404
|
# ==== Using :all
|
|
439
405
|
#
|
|
440
406
|
# The :on_duplicate_key_update option can be set to :all. All columns
|
|
441
|
-
# 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
|
|
442
409
|
# supplied, only those columns will be updated. Below is an example:
|
|
443
410
|
#
|
|
444
411
|
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
|
@@ -469,7 +436,8 @@ class ActiveRecord::Base
|
|
|
469
436
|
# ==== Using :all
|
|
470
437
|
#
|
|
471
438
|
# The :on_duplicate_key_update option can be set to :all. All columns
|
|
472
|
-
# 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
|
|
473
441
|
# supplied, only those columns will be updated. Below is an example:
|
|
474
442
|
#
|
|
475
443
|
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
|
@@ -534,7 +502,8 @@ class ActiveRecord::Base
|
|
|
534
502
|
#
|
|
535
503
|
# ===== Using :all
|
|
536
504
|
#
|
|
537
|
-
# 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.
|
|
538
507
|
# If a list of column names is supplied, only those columns will be updated.
|
|
539
508
|
# Below is an example:
|
|
540
509
|
#
|
|
@@ -573,160 +542,58 @@ class ActiveRecord::Base
|
|
|
573
542
|
import_helper(*args)
|
|
574
543
|
end
|
|
575
544
|
end
|
|
576
|
-
alias import bulk_import unless ActiveRecord::Base.respond_to? :import
|
|
577
545
|
|
|
578
546
|
# Imports a collection of values if all values are valid. Import fails at the
|
|
579
547
|
# first encountered validation error and raises ActiveRecord::RecordInvalid
|
|
580
548
|
# with the failed instance.
|
|
581
549
|
def bulk_import!(*args)
|
|
582
550
|
options = args.last.is_a?( Hash ) ? args.pop : {}
|
|
583
|
-
options
|
|
584
|
-
options[:raise_error] = true
|
|
551
|
+
options = options.merge( validate: true, raise_error: true )
|
|
585
552
|
|
|
586
553
|
bulk_import(*args, options)
|
|
587
554
|
end
|
|
588
|
-
|
|
555
|
+
|
|
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
|
|
589
563
|
|
|
590
564
|
def import_helper( *args )
|
|
591
565
|
connection_pool.with_connection do |conn|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
options
|
|
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)
|
|
569
|
+
|
|
570
|
+
options, args = options_and_arguments_for_import(args)
|
|
597
571
|
|
|
598
572
|
is_validating = options[:validate_with_context].present? || options[:validate]
|
|
599
|
-
validator = ActiveRecord::Import::Validator.new(
|
|
573
|
+
validator = ActiveRecord::Import::Validator.new(options) if is_validating
|
|
574
|
+
|
|
575
|
+
# supports empty array
|
|
576
|
+
return ActiveRecord::Import::Result.new([], 0, [], []) if args.last.is_a?( Array ) && args.last.empty?
|
|
600
577
|
|
|
601
578
|
# assume array of model objects
|
|
602
579
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
column_names = args.first.dup
|
|
606
|
-
else
|
|
607
|
-
models = args.first
|
|
608
|
-
column_names = if conn.try(:supports_virtual_columns?)
|
|
609
|
-
columns.reject(&:virtual?).map(&:name)
|
|
610
|
-
else
|
|
611
|
-
self.column_names.dup
|
|
612
|
-
end
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
if models.first.id.nil?
|
|
616
|
-
Array(primary_key).each do |c|
|
|
617
|
-
if column_names.include?(c) && schema_columns_hash(conn)[c].type == :uuid
|
|
618
|
-
column_names.delete(c)
|
|
619
|
-
end
|
|
620
|
-
end
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
update_attrs = if record_timestamps && options[:timestamps]
|
|
624
|
-
if respond_to?(:timestamp_attributes_for_update, true)
|
|
625
|
-
send(:timestamp_attributes_for_update).map(&:to_sym)
|
|
626
|
-
else
|
|
627
|
-
allocate.send(:timestamp_attributes_for_update_in_model)
|
|
628
|
-
end
|
|
629
|
-
end
|
|
630
|
-
|
|
631
|
-
array_of_attributes = []
|
|
632
|
-
|
|
633
|
-
models.each do |model|
|
|
634
|
-
if supports_setting_primary_key_of_imported_objects?(conn)
|
|
635
|
-
load_association_ids(model)
|
|
636
|
-
end
|
|
637
|
-
|
|
638
|
-
if is_validating && !validator.valid_model?(model)
|
|
639
|
-
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
|
640
|
-
next
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
array_of_attributes << column_names.map do |name|
|
|
644
|
-
if model.persisted? &&
|
|
645
|
-
update_attrs && update_attrs.include?(name.to_sym) &&
|
|
646
|
-
!model.send("#{name}_changed?")
|
|
647
|
-
nil
|
|
648
|
-
else
|
|
649
|
-
model.read_attribute(name.to_s)
|
|
650
|
-
end
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
# supports array of hash objects
|
|
654
|
-
elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
|
|
655
|
-
if args.length == 2
|
|
656
|
-
array_of_hashes = args.last
|
|
657
|
-
column_names = args.first.dup
|
|
658
|
-
allow_extra_hash_keys = true
|
|
659
|
-
else
|
|
660
|
-
array_of_hashes = args.first
|
|
661
|
-
column_names = array_of_hashes.first.keys
|
|
662
|
-
allow_extra_hash_keys = false
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
array_of_attributes = array_of_hashes.map do |h|
|
|
666
|
-
error_message = validate_hash_import(h, column_names, allow_extra_hash_keys)
|
|
667
|
-
|
|
668
|
-
raise ArgumentError, error_message if error_message
|
|
669
|
-
|
|
670
|
-
column_names.map do |key|
|
|
671
|
-
h[key]
|
|
672
|
-
end
|
|
673
|
-
end
|
|
674
|
-
# supports empty array
|
|
675
|
-
elsif args.last.is_a?( Array ) && args.last.empty?
|
|
676
|
-
return ActiveRecord::Import::Result.new([], 0, [], [])
|
|
677
|
-
# supports 2-element array and array
|
|
678
|
-
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
|
679
|
-
|
|
680
|
-
unless args.last.first.is_a?(Array)
|
|
681
|
-
raise ArgumentError, "Last argument should be a two dimensional array '[[]]'. First element in array was a #{args.last.first.class}"
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
column_names, array_of_attributes = args
|
|
685
|
-
|
|
686
|
-
# dup the passed args so we don't modify unintentionally
|
|
687
|
-
column_names = column_names.dup
|
|
688
|
-
array_of_attributes = array_of_attributes.map(&:dup)
|
|
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)
|
|
689
582
|
else
|
|
690
|
-
|
|
583
|
+
column_names, array_of_attributes = column_names_and_attributes_for_import(args)
|
|
691
584
|
end
|
|
692
585
|
|
|
693
|
-
#
|
|
694
|
-
|
|
695
|
-
# value for it into each row so the sequencer will fire later
|
|
696
|
-
symbolized_column_names = Array(column_names).map(&:to_sym)
|
|
697
|
-
symbolized_primary_key = Array(primary_key).map(&:to_sym)
|
|
698
|
-
|
|
699
|
-
if !symbolized_primary_key.to_set.subset?(symbolized_column_names.to_set) && conn.prefetch_primary_key? && sequence_name
|
|
700
|
-
column_count = column_names.size
|
|
701
|
-
column_names.concat(Array(primary_key)).uniq!
|
|
702
|
-
columns_added = column_names.size - column_count
|
|
703
|
-
new_fields = Array.new(columns_added)
|
|
704
|
-
array_of_attributes.each { |a| a.concat(new_fields) }
|
|
705
|
-
end
|
|
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)
|
|
706
588
|
|
|
707
589
|
# Don't modify incoming arguments
|
|
708
590
|
on_duplicate_key_update = options[:on_duplicate_key_update]
|
|
709
591
|
if on_duplicate_key_update
|
|
710
|
-
|
|
711
|
-
options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
|
|
712
|
-
on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
|
|
713
|
-
duped_options[k] = if k == :columns && v == :all
|
|
714
|
-
updatable_columns
|
|
715
|
-
elsif v.duplicable?
|
|
716
|
-
v.dup
|
|
717
|
-
else
|
|
718
|
-
v
|
|
719
|
-
end
|
|
720
|
-
end
|
|
721
|
-
elsif on_duplicate_key_update == :all
|
|
722
|
-
updatable_columns
|
|
723
|
-
elsif on_duplicate_key_update.duplicable?
|
|
724
|
-
on_duplicate_key_update.dup
|
|
725
|
-
else
|
|
726
|
-
on_duplicate_key_update
|
|
727
|
-
end
|
|
592
|
+
options[:on_duplicate_key_update] = on_duplicate_key_update_for_import(on_duplicate_key_update, column_names, options)
|
|
728
593
|
end
|
|
729
594
|
|
|
595
|
+
add_primary_key_for_sequence(conn, column_names, array_of_attributes)
|
|
596
|
+
|
|
730
597
|
timestamps = {}
|
|
731
598
|
|
|
732
599
|
# record timestamps unless disabled in ActiveRecord::Base
|
|
@@ -737,41 +604,20 @@ class ActiveRecord::Base
|
|
|
737
604
|
return_obj = if is_validating
|
|
738
605
|
import_with_validations( conn, column_names, array_of_attributes, options ) do |failed_instances|
|
|
739
606
|
if models
|
|
740
|
-
models
|
|
741
|
-
next unless m.errors.any?
|
|
742
|
-
|
|
743
|
-
failed_instances << (options[:track_validation_failures] ? [i, m] : m)
|
|
744
|
-
end
|
|
607
|
+
add_failed_models(failed_instances, models, options)
|
|
745
608
|
else
|
|
746
|
-
|
|
747
|
-
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
|
748
|
-
|
|
749
|
-
# keep track of the instance and the position it is currently at. if this fails
|
|
750
|
-
# validation we'll use the index to remove it from the array_of_attributes
|
|
751
|
-
arr.each_with_index do |hsh, i|
|
|
752
|
-
# utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
|
|
753
|
-
model = new do |m|
|
|
754
|
-
hsh.each_pair { |k, v| m[k] = v }
|
|
755
|
-
end
|
|
756
|
-
|
|
757
|
-
next if validator.valid_model?(model)
|
|
758
|
-
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
|
759
|
-
|
|
760
|
-
array_of_attributes[i] = nil
|
|
761
|
-
failure = model.dup
|
|
762
|
-
failure.errors.send(:initialize_dup, model.errors)
|
|
763
|
-
failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
|
|
764
|
-
end
|
|
765
|
-
array_of_attributes.compact!
|
|
609
|
+
validate_attributes_for_import(failed_instances, column_names, array_of_attributes, validator, options)
|
|
766
610
|
end
|
|
767
611
|
end
|
|
768
612
|
else
|
|
769
613
|
import_without_validations_or_callbacks( conn, column_names, array_of_attributes, options )
|
|
770
614
|
end
|
|
771
615
|
|
|
616
|
+
return return_obj if options[:all_or_none] && return_obj.failed_instances.any?
|
|
617
|
+
|
|
772
618
|
if options[:synchronize]
|
|
773
619
|
sync_keys = options[:synchronize_keys] || Array(primary_key)
|
|
774
|
-
|
|
620
|
+
synchronized_instances = synchronize_imported_instances(options[:synchronize], sync_keys)
|
|
775
621
|
end
|
|
776
622
|
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
|
777
623
|
|
|
@@ -781,11 +627,14 @@ class ActiveRecord::Base
|
|
|
781
627
|
|
|
782
628
|
# if there are auto-save associations on the models we imported that are new, import them as well
|
|
783
629
|
if options[:recursive]
|
|
784
|
-
|
|
785
|
-
import_associations(models, options.dup.merge(validate: false))
|
|
630
|
+
import_associations_of_imported_models(models, return_obj, on_duplicate_key_update, options)
|
|
786
631
|
end
|
|
787
632
|
end
|
|
788
633
|
|
|
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? }
|
|
637
|
+
|
|
789
638
|
return_obj
|
|
790
639
|
end
|
|
791
640
|
end
|
|
@@ -820,8 +669,7 @@ class ActiveRecord::Base
|
|
|
820
669
|
return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
|
|
821
670
|
|
|
822
671
|
column_names = column_names.map do |name|
|
|
823
|
-
|
|
824
|
-
original_name.to_sym
|
|
672
|
+
column_name_for_import(name).to_sym
|
|
825
673
|
end
|
|
826
674
|
scope_columns, scope_values = scope_attributes.to_a.transpose
|
|
827
675
|
|
|
@@ -849,7 +697,8 @@ class ActiveRecord::Base
|
|
|
849
697
|
pre_sql_statements = conn.pre_sql_statements( options )
|
|
850
698
|
insert_sql = ['INSERT', pre_sql_statements, "INTO #{quoted_table_name} #{columns_sql} VALUES "]
|
|
851
699
|
insert_sql = insert_sql.flatten.join(' ')
|
|
852
|
-
values_sql = values_sql_for_columns_and_attributes(conn, 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)
|
|
853
702
|
|
|
854
703
|
number_inserted = 0
|
|
855
704
|
ids = []
|
|
@@ -859,6 +708,7 @@ class ActiveRecord::Base
|
|
|
859
708
|
if supports_import?(conn)
|
|
860
709
|
# generate the sql
|
|
861
710
|
post_sql_statements = conn.post_sql_statements( quoted_table_name, options )
|
|
711
|
+
post_sql_statements += [comment_sql] if comment_sql
|
|
862
712
|
import_size = values_sql.size
|
|
863
713
|
|
|
864
714
|
batch_size = options[:batch_size] || import_size
|
|
@@ -877,17 +727,21 @@ class ActiveRecord::Base
|
|
|
877
727
|
"#{model_name} Create Many" )
|
|
878
728
|
|
|
879
729
|
number_inserted += result.num_inserts
|
|
880
|
-
ids
|
|
881
|
-
results
|
|
730
|
+
ids.concat(result.ids)
|
|
731
|
+
results.concat(result.results)
|
|
882
732
|
affected_rows = result.affected_rows.nil? ? nil : (affected_rows || 0) + result.affected_rows
|
|
883
733
|
current_batch += 1
|
|
884
734
|
|
|
885
735
|
progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
|
|
886
736
|
end
|
|
887
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
|
+
|
|
888
742
|
transaction(requires_new: true) do
|
|
889
743
|
values_sql.each do |values|
|
|
890
|
-
ids << conn.insert(insert_sql
|
|
744
|
+
ids << conn.insert("#{insert_sql}#{values}#{comment_sql}", "#{model_name} Create Many")
|
|
891
745
|
number_inserted += 1
|
|
892
746
|
end
|
|
893
747
|
end
|
|
@@ -897,6 +751,239 @@ class ActiveRecord::Base
|
|
|
897
751
|
|
|
898
752
|
private
|
|
899
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
|
+
|
|
900
987
|
def associated_options(options, association)
|
|
901
988
|
return options unless options.key?(:recursive_on_duplicate_key_update)
|
|
902
989
|
|
|
@@ -905,9 +992,55 @@ class ActiveRecord::Base
|
|
|
905
992
|
)
|
|
906
993
|
end
|
|
907
994
|
|
|
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
|
+
|
|
908
1041
|
def set_attributes_and_mark_clean(conn, models, import_result, timestamps, options)
|
|
909
1042
|
return if models.nil?
|
|
910
|
-
models
|
|
1043
|
+
models = imported_models(models, import_result, options)
|
|
911
1044
|
|
|
912
1045
|
# if ids were returned for all models we know all were updated
|
|
913
1046
|
if models.size == import_result.ids.size
|
|
@@ -924,20 +1057,18 @@ class ActiveRecord::Base
|
|
|
924
1057
|
deserialize_value = lambda do |column, value|
|
|
925
1058
|
column = schema_columns_hash(conn)[column]
|
|
926
1059
|
return value unless column
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
type.deserialize(value)
|
|
930
|
-
elsif column.respond_to?(:type_cast_from_database)
|
|
931
|
-
column.type_cast_from_database(value)
|
|
932
|
-
else
|
|
933
|
-
value
|
|
934
|
-
end
|
|
1060
|
+
type = type_for_attribute(column.name)
|
|
1061
|
+
type.deserialize(value)
|
|
935
1062
|
end
|
|
936
1063
|
|
|
937
1064
|
set_value = lambda do |model, column, value|
|
|
938
1065
|
val = deserialize_value.call(column, value)
|
|
939
1066
|
if model.attribute_names.include?(column)
|
|
940
|
-
|
|
1067
|
+
if write_id_column_directly?(column)
|
|
1068
|
+
model._write_attribute(column, val)
|
|
1069
|
+
else
|
|
1070
|
+
model.send("#{column}=", val)
|
|
1071
|
+
end
|
|
941
1072
|
else
|
|
942
1073
|
attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
|
|
943
1074
|
model.instance_variable_set(:@attributes, attributes)
|
|
@@ -962,14 +1093,7 @@ class ActiveRecord::Base
|
|
|
962
1093
|
end
|
|
963
1094
|
|
|
964
1095
|
models.each do |model|
|
|
965
|
-
|
|
966
|
-
model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
|
|
967
|
-
model.changes_applied
|
|
968
|
-
elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
|
|
969
|
-
model.clear_changes_information
|
|
970
|
-
else # Rails 3.2
|
|
971
|
-
model.instance_variable_get(:@changed_attributes).clear
|
|
972
|
-
end
|
|
1096
|
+
model.changes_applied
|
|
973
1097
|
model.instance_variable_set(:@new_record, false)
|
|
974
1098
|
end
|
|
975
1099
|
end
|
|
@@ -989,12 +1113,24 @@ class ActiveRecord::Base
|
|
|
989
1113
|
association = association.target
|
|
990
1114
|
next if association.blank? || model.public_send(column_name).present?
|
|
991
1115
|
|
|
992
|
-
association_primary_key =
|
|
1116
|
+
association_primary_key = association_primary_key_names(association_reflection)[column_index]
|
|
1117
|
+
next unless association_primary_key
|
|
1118
|
+
|
|
993
1119
|
model.public_send("#{column_name}=", association.send(association_primary_key))
|
|
994
1120
|
end
|
|
995
1121
|
end
|
|
996
1122
|
end
|
|
997
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
|
+
|
|
998
1134
|
def import_associations(models, options)
|
|
999
1135
|
# now, for all the dirty associations, collect them into a new set of models, then recurse.
|
|
1000
1136
|
# notes:
|
|
@@ -1020,8 +1156,45 @@ class ActiveRecord::Base
|
|
|
1020
1156
|
end
|
|
1021
1157
|
end
|
|
1022
1158
|
|
|
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
|
+
|
|
1023
1196
|
def schema_columns_hash(conn)
|
|
1024
|
-
if
|
|
1197
|
+
if ignored_columns.any?
|
|
1025
1198
|
conn.schema_cache.columns_hash(table_name)
|
|
1026
1199
|
else
|
|
1027
1200
|
columns_hash
|
|
@@ -1046,18 +1219,25 @@ class ActiveRecord::Base
|
|
|
1046
1219
|
# Wrap target in an array if not already
|
|
1047
1220
|
association = Array(association.target)
|
|
1048
1221
|
|
|
1049
|
-
|
|
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
|
+
|
|
1050
1236
|
changed_objects.each do |child|
|
|
1051
|
-
|
|
1052
|
-
child.public_send("#{column}=",
|
|
1237
|
+
foreign_key_columns.each_with_index do |column, index|
|
|
1238
|
+
child.public_send("#{column}=", foreign_key_values[index])
|
|
1053
1239
|
end
|
|
1054
1240
|
|
|
1055
|
-
# For polymorphic associations
|
|
1056
|
-
association_name = if model.class.respond_to?(:polymorphic_name)
|
|
1057
|
-
model.class.polymorphic_name
|
|
1058
|
-
else
|
|
1059
|
-
model.class.base_class
|
|
1060
|
-
end
|
|
1061
1241
|
association_reflection.type.try do |type|
|
|
1062
1242
|
child.public_send("#{type}=", association_name)
|
|
1063
1243
|
end
|
|
@@ -1067,33 +1247,55 @@ class ActiveRecord::Base
|
|
|
1067
1247
|
associated_objects_by_class
|
|
1068
1248
|
end
|
|
1069
1249
|
|
|
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?
|
|
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
|
+
|
|
1070
1272
|
# Returns SQL the VALUES for an INSERT statement given the passed in +columns+
|
|
1071
|
-
# and +array_of_attributes+.
|
|
1072
|
-
|
|
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:
|
|
1073
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
|
+
|
|
1074
1281
|
my_values = arr.each_with_index.map do |val, j|
|
|
1075
1282
|
column = columns[j]
|
|
1076
1283
|
|
|
1077
1284
|
# be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
|
|
1078
1285
|
if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
|
|
1079
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"
|
|
1080
1293
|
elsif val.respond_to?(:to_sql)
|
|
1081
1294
|
"(#{val.to_sql})"
|
|
1082
1295
|
elsif column
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
conn.quote(val)
|
|
1087
|
-
elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
|
|
1088
|
-
conn.quote(column.type_cast_from_user(val), column)
|
|
1089
|
-
else # Rails 3.2, 4.0 and 4.1
|
|
1090
|
-
if serialized_attributes.include?(column.name)
|
|
1091
|
-
val = serialized_attributes[column.name].dump(val)
|
|
1092
|
-
end
|
|
1093
|
-
# Fixes #443 to support binary (i.e. bytea) columns on PG
|
|
1094
|
-
val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
|
|
1095
|
-
conn.quote(val, column)
|
|
1096
|
-
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)
|
|
1097
1299
|
else
|
|
1098
1300
|
raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
|
|
1099
1301
|
end
|
|
@@ -1106,14 +1308,8 @@ class ActiveRecord::Base
|
|
|
1106
1308
|
timestamp_columns = {}
|
|
1107
1309
|
timestamps = {}
|
|
1108
1310
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
|
1112
|
-
else
|
|
1113
|
-
instance = allocate
|
|
1114
|
-
timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
|
|
1115
|
-
timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
|
|
1116
|
-
end
|
|
1311
|
+
timestamp_columns[:create] = timestamp_attributes_for_create_in_model
|
|
1312
|
+
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
|
1117
1313
|
|
|
1118
1314
|
# use tz as set in ActiveRecord::Base
|
|
1119
1315
|
default_timezone = if ActiveRecord.respond_to?(:default_timezone)
|
|
@@ -1146,6 +1342,17 @@ class ActiveRecord::Base
|
|
|
1146
1342
|
timestamps
|
|
1147
1343
|
end
|
|
1148
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
|
+
|
|
1149
1356
|
# Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
|
|
1150
1357
|
def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
|
|
1151
1358
|
array_of_attributes.map { |values| Hash[column_names.zip(values)] }
|