activerecord-import 0.25.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/test.yaml +151 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +74 -8
- data/.rubocop_todo.yml +10 -16
- data/Brewfile +3 -1
- data/CHANGELOG.md +232 -2
- data/Dockerfile +23 -0
- data/Gemfile +26 -14
- data/LICENSE +21 -56
- data/README.markdown +612 -21
- data/Rakefile +4 -1
- data/activerecord-import.gemspec +6 -5
- data/benchmarks/benchmark.rb +10 -4
- data/benchmarks/lib/base.rb +4 -2
- data/benchmarks/lib/cli_parser.rb +4 -2
- data/benchmarks/lib/float.rb +2 -0
- data/benchmarks/lib/mysql2_benchmark.rb +2 -0
- data/benchmarks/lib/output_to_csv.rb +2 -0
- data/benchmarks/lib/output_to_html.rb +4 -2
- data/benchmarks/models/test_innodb.rb +2 -0
- data/benchmarks/models/test_memory.rb +2 -0
- data/benchmarks/models/test_myisam.rb +2 -0
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
- data/docker-compose.yml +34 -0
- data/gemfiles/4.2.gemfile +2 -0
- data/gemfiles/5.0.gemfile +2 -0
- data/gemfiles/5.1.gemfile +2 -0
- data/gemfiles/5.2.gemfile +2 -0
- data/gemfiles/6.0.gemfile +4 -0
- data/gemfiles/6.1.gemfile +4 -0
- data/gemfiles/7.0.gemfile +4 -0
- data/gemfiles/7.1.gemfile +3 -0
- data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
- data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/trilogy_adapter.rb +8 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +15 -6
- data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +34 -29
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +74 -55
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +138 -13
- data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
- data/lib/activerecord-import/base.rb +11 -2
- data/lib/activerecord-import/import.rb +290 -114
- data/lib/activerecord-import/mysql2.rb +2 -0
- data/lib/activerecord-import/postgresql.rb +2 -0
- data/lib/activerecord-import/sqlite3.rb +2 -0
- data/lib/activerecord-import/synchronize.rb +4 -2
- data/lib/activerecord-import/value_sets_parser.rb +5 -0
- data/lib/activerecord-import/version.rb +3 -1
- data/lib/activerecord-import.rb +2 -1
- data/test/adapters/jdbcmysql.rb +2 -0
- data/test/adapters/jdbcpostgresql.rb +2 -0
- data/test/adapters/jdbcsqlite3.rb +2 -0
- data/test/adapters/makara_postgis.rb +2 -0
- data/test/adapters/mysql2.rb +2 -0
- data/test/adapters/mysql2_makara.rb +2 -0
- data/test/adapters/mysql2spatial.rb +2 -0
- data/test/adapters/postgis.rb +2 -0
- data/test/adapters/postgresql.rb +2 -0
- data/test/adapters/postgresql_makara.rb +2 -0
- data/test/adapters/seamless_database_pool.rb +2 -0
- data/test/adapters/spatialite.rb +2 -0
- data/test/adapters/sqlite3.rb +2 -0
- data/test/adapters/trilogy.rb +9 -0
- data/test/database.yml.sample +7 -0
- data/test/{travis → github}/database.yml +7 -1
- data/test/import_test.rb +151 -8
- data/test/jdbcmysql/import_test.rb +5 -3
- data/test/jdbcpostgresql/import_test.rb +4 -2
- data/test/jdbcsqlite3/import_test.rb +4 -2
- data/test/makara_postgis/import_test.rb +4 -2
- data/test/models/account.rb +2 -0
- data/test/models/alarm.rb +2 -0
- data/test/models/animal.rb +8 -0
- data/test/models/author.rb +7 -0
- data/test/models/bike_maker.rb +3 -0
- data/test/models/book.rb +7 -2
- data/test/models/car.rb +2 -0
- data/test/models/card.rb +5 -0
- data/test/models/chapter.rb +2 -0
- data/test/models/composite_book.rb +19 -0
- data/test/models/composite_chapter.rb +9 -0
- data/test/models/customer.rb +18 -0
- data/test/models/deck.rb +8 -0
- data/test/models/dictionary.rb +2 -0
- data/test/models/discount.rb +2 -0
- data/test/models/end_note.rb +2 -0
- data/test/models/group.rb +2 -0
- data/test/models/order.rb +17 -0
- data/test/models/playing_card.rb +4 -0
- data/test/models/promotion.rb +2 -0
- data/test/models/question.rb +2 -0
- data/test/models/rule.rb +2 -0
- data/test/models/tag.rb +9 -1
- data/test/models/tag_alias.rb +11 -0
- data/test/models/topic.rb +7 -0
- data/test/models/user.rb +2 -0
- data/test/models/user_token.rb +3 -0
- data/test/models/vendor.rb +2 -0
- data/test/models/widget.rb +2 -0
- data/test/mysql2/import_test.rb +5 -3
- data/test/mysql2_makara/import_test.rb +5 -3
- data/test/mysqlspatial2/import_test.rb +5 -3
- data/test/postgis/import_test.rb +4 -2
- data/test/postgresql/import_test.rb +4 -2
- data/test/schema/generic_schema.rb +37 -1
- data/test/schema/jdbcpostgresql_schema.rb +3 -1
- data/test/schema/mysql2_schema.rb +2 -0
- data/test/schema/postgis_schema.rb +3 -1
- data/test/schema/postgresql_schema.rb +49 -0
- data/test/schema/sqlite3_schema.rb +15 -0
- data/test/schema/version.rb +2 -0
- data/test/sqlite3/import_test.rb +4 -2
- data/test/support/active_support/test_case_extensions.rb +2 -0
- data/test/support/assertions.rb +2 -0
- data/test/support/factories.rb +10 -8
- data/test/support/generate.rb +10 -8
- data/test/support/mysql/import_examples.rb +2 -1
- data/test/support/postgresql/import_examples.rb +152 -3
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +122 -9
- data/test/support/shared_examples/recursive_import.rb +128 -2
- data/test/support/sqlite3/import_examples.rb +191 -26
- data/test/synchronize_test.rb +2 -0
- data/test/test_helper.rb +34 -7
- data/test/trilogy/import_test.rb +7 -0
- data/test/value_sets_bytes_parser_test.rb +3 -1
- data/test/value_sets_records_parser_test.rb +3 -1
- metadata +46 -16
- data/.travis.yml +0 -71
- data/gemfiles/3.2.gemfile +0 -2
- data/gemfiles/4.0.gemfile +0 -2
- data/gemfiles/4.1.gemfile +0 -2
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "ostruct"
|
|
2
4
|
|
|
3
5
|
module ActiveRecord::Import::ConnectionAdapters; end
|
|
4
6
|
|
|
5
|
-
module ActiveRecord::Import
|
|
7
|
+
module ActiveRecord::Import # :nodoc:
|
|
6
8
|
Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
|
|
7
9
|
|
|
8
|
-
module ImportSupport
|
|
9
|
-
def supports_import?
|
|
10
|
+
module ImportSupport # :nodoc:
|
|
11
|
+
def supports_import? # :nodoc:
|
|
10
12
|
true
|
|
11
13
|
end
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
module OnDuplicateKeyUpdateSupport
|
|
15
|
-
def supports_on_duplicate_key_update?
|
|
16
|
+
module OnDuplicateKeyUpdateSupport # :nodoc:
|
|
17
|
+
def supports_on_duplicate_key_update? # :nodoc:
|
|
16
18
|
true
|
|
17
19
|
end
|
|
18
20
|
end
|
|
@@ -24,33 +26,70 @@ module ActiveRecord::Import #:nodoc:
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
class Validator
|
|
27
|
-
def initialize(options = {})
|
|
29
|
+
def initialize(klass, options = {})
|
|
28
30
|
@options = options
|
|
31
|
+
@validator_class = klass
|
|
32
|
+
init_validations(klass)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def init_validations(klass)
|
|
36
|
+
@validate_callbacks = klass._validate_callbacks.dup
|
|
37
|
+
|
|
38
|
+
@validate_callbacks.each_with_index do |callback, i|
|
|
39
|
+
filter = callback.respond_to?(:raw_filter) ? callback.raw_filter : callback.filter
|
|
40
|
+
next unless filter.class.name =~ /Validations::PresenceValidator/ ||
|
|
41
|
+
(!@options[:validate_uniqueness] &&
|
|
42
|
+
filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
|
|
43
|
+
|
|
44
|
+
callback = callback.dup
|
|
45
|
+
filter = filter.dup
|
|
46
|
+
attrs = filter.instance_variable_get(:@attributes).dup
|
|
47
|
+
|
|
48
|
+
if filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
|
49
|
+
attrs = []
|
|
50
|
+
else
|
|
51
|
+
associations = klass.reflect_on_all_associations(:belongs_to)
|
|
52
|
+
associations.each do |assoc|
|
|
53
|
+
if (index = attrs.index(assoc.name))
|
|
54
|
+
key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
|
|
55
|
+
attrs[index] = key unless attrs.include?(key)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
filter.instance_variable_set(:@attributes, attrs.flatten)
|
|
61
|
+
|
|
62
|
+
if @validate_callbacks.respond_to?(:chain, true)
|
|
63
|
+
@validate_callbacks.send(:chain).tap do |chain|
|
|
64
|
+
callback.instance_variable_set(:@filter, filter)
|
|
65
|
+
chain[i] = callback
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
callback.raw_filter = filter
|
|
69
|
+
callback.filter = callback.send(:_compile_filter, filter)
|
|
70
|
+
@validate_callbacks[i] = callback
|
|
71
|
+
end
|
|
72
|
+
end
|
|
29
73
|
end
|
|
30
74
|
|
|
31
75
|
def valid_model?(model)
|
|
76
|
+
init_validations(model.class) unless model.instance_of?(@validator_class)
|
|
77
|
+
|
|
32
78
|
validation_context = @options[:validate_with_context]
|
|
33
79
|
validation_context ||= (model.new_record? ? :create : :update)
|
|
34
|
-
|
|
35
80
|
current_context = model.send(:validation_context)
|
|
81
|
+
|
|
36
82
|
begin
|
|
37
83
|
model.send(:validation_context=, validation_context)
|
|
38
84
|
model.errors.clear
|
|
39
85
|
|
|
40
|
-
validate_callbacks = model._validate_callbacks.dup
|
|
41
|
-
associations = model.class.reflect_on_all_associations(:belongs_to).map(&:name)
|
|
42
|
-
|
|
43
|
-
model._validate_callbacks.each do |callback|
|
|
44
|
-
filter = callback.raw_filter
|
|
45
|
-
if filter.is_a?(ActiveRecord::Validations::UniquenessValidator) ||
|
|
46
|
-
(defined?(ActiveRecord::Validations::PresenceValidator) && filter.is_a?(ActiveRecord::Validations::PresenceValidator) && associations.include?(filter.attributes.first))
|
|
47
|
-
validate_callbacks.delete(callback)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
86
|
model.run_callbacks(:validation) do
|
|
52
87
|
if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
|
|
53
|
-
runner = validate_callbacks.compile
|
|
88
|
+
runner = if @validate_callbacks.method(:compile).arity == 0
|
|
89
|
+
@validate_callbacks.compile
|
|
90
|
+
else # ActiveRecord >= 7.1
|
|
91
|
+
@validate_callbacks.compile(nil)
|
|
92
|
+
end
|
|
54
93
|
env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
|
|
55
94
|
if runner.respond_to?(:call) # ActiveRecord < 5.1
|
|
56
95
|
runner.call(env)
|
|
@@ -69,10 +108,10 @@ module ActiveRecord::Import #:nodoc:
|
|
|
69
108
|
runner.invoke_before(env)
|
|
70
109
|
runner.invoke_after(env)
|
|
71
110
|
end
|
|
72
|
-
elsif validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
|
73
|
-
model.instance_eval validate_callbacks.compile
|
|
111
|
+
elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
|
112
|
+
model.instance_eval @validate_callbacks.compile
|
|
74
113
|
else # ActiveRecord 3.x
|
|
75
|
-
model.instance_eval validate_callbacks.compile(nil, model)
|
|
114
|
+
model.instance_eval @validate_callbacks.compile(nil, model)
|
|
76
115
|
end
|
|
77
116
|
end
|
|
78
117
|
|
|
@@ -130,7 +169,7 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
130
169
|
m.public_send "#{reflection.type}=", owner.class.name if reflection.type
|
|
131
170
|
end
|
|
132
171
|
|
|
133
|
-
|
|
172
|
+
model_klass.bulk_import column_names, models, options
|
|
134
173
|
|
|
135
174
|
# supports array of hash objects
|
|
136
175
|
elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
|
|
@@ -169,11 +208,11 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
169
208
|
end
|
|
170
209
|
end
|
|
171
210
|
|
|
172
|
-
|
|
211
|
+
model_klass.bulk_import column_names, array_of_attributes, options
|
|
173
212
|
|
|
174
213
|
# supports empty array
|
|
175
214
|
elsif args.last.is_a?( Array ) && args.last.empty?
|
|
176
|
-
|
|
215
|
+
ActiveRecord::Import::Result.new([], 0, [])
|
|
177
216
|
|
|
178
217
|
# supports 2-element array and array
|
|
179
218
|
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
|
@@ -204,7 +243,7 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
204
243
|
end
|
|
205
244
|
end
|
|
206
245
|
|
|
207
|
-
|
|
246
|
+
model_klass.bulk_import column_names, array_of_attributes, options
|
|
208
247
|
else
|
|
209
248
|
raise ArgumentError, "Invalid arguments!"
|
|
210
249
|
end
|
|
@@ -212,16 +251,17 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
212
251
|
alias import bulk_import unless respond_to? :import
|
|
213
252
|
end
|
|
214
253
|
|
|
254
|
+
module ActiveRecord::Import::Connection
|
|
255
|
+
def establish_connection(args = nil)
|
|
256
|
+
conn = super(args)
|
|
257
|
+
ActiveRecord::Import.load_from_connection_pool connection_pool
|
|
258
|
+
conn
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
215
262
|
class ActiveRecord::Base
|
|
216
263
|
class << self
|
|
217
|
-
|
|
218
|
-
conn = establish_connection_without_activerecord_import(*args)
|
|
219
|
-
ActiveRecord::Import.load_from_connection_pool connection_pool
|
|
220
|
-
conn
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
alias establish_connection_without_activerecord_import establish_connection
|
|
224
|
-
alias establish_connection establish_connection_with_activerecord_import
|
|
264
|
+
prepend ActiveRecord::Import::Connection
|
|
225
265
|
|
|
226
266
|
# Returns true if the current database connection adapter
|
|
227
267
|
# supports import functionality, otherwise returns false.
|
|
@@ -298,8 +338,8 @@ class ActiveRecord::Base
|
|
|
298
338
|
# recursive import. For database adapters that normally support
|
|
299
339
|
# setting primary keys on imported objects, this option prevents
|
|
300
340
|
# that from occurring.
|
|
301
|
-
# * +on_duplicate_key_update+ - an Array or Hash, tells import to
|
|
302
|
-
# use MySQL's ON DUPLICATE KEY UPDATE or Postgres
|
|
341
|
+
# * +on_duplicate_key_update+ - :all, an Array, or Hash, tells import to
|
|
342
|
+
# use MySQL's ON DUPLICATE KEY UPDATE or Postgres/SQLite ON CONFLICT
|
|
303
343
|
# DO UPDATE ability. See On Duplicate Key Update below.
|
|
304
344
|
# * +synchronize+ - an array of ActiveRecord instances for the model
|
|
305
345
|
# that you are currently importing data into. This synchronizes
|
|
@@ -358,7 +398,15 @@ class ActiveRecord::Base
|
|
|
358
398
|
#
|
|
359
399
|
# == On Duplicate Key Update (MySQL)
|
|
360
400
|
#
|
|
361
|
-
# The :on_duplicate_key_update option can be either an Array or a Hash.
|
|
401
|
+
# The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
|
|
402
|
+
#
|
|
403
|
+
# ==== Using :all
|
|
404
|
+
#
|
|
405
|
+
# The :on_duplicate_key_update option can be set to :all. All columns
|
|
406
|
+
# other than the primary key are updated. If a list of column names is
|
|
407
|
+
# supplied, only those columns will be updated. Below is an example:
|
|
408
|
+
#
|
|
409
|
+
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
|
362
410
|
#
|
|
363
411
|
# ==== Using an Array
|
|
364
412
|
#
|
|
@@ -377,11 +425,19 @@ class ActiveRecord::Base
|
|
|
377
425
|
#
|
|
378
426
|
# BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
|
|
379
427
|
#
|
|
380
|
-
# == On Duplicate Key Update (Postgres 9.5+)
|
|
428
|
+
# == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
|
|
381
429
|
#
|
|
382
|
-
# The :on_duplicate_key_update option can be an Array or a Hash with up to
|
|
430
|
+
# The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
|
|
383
431
|
# three attributes, :conflict_target (and optionally :index_predicate) or
|
|
384
|
-
# :constraint_name, and :columns.
|
|
432
|
+
# :constraint_name (Postgres), and :columns.
|
|
433
|
+
#
|
|
434
|
+
# ==== Using :all
|
|
435
|
+
#
|
|
436
|
+
# The :on_duplicate_key_update option can be set to :all. All columns
|
|
437
|
+
# other than the primary key are updated. If a list of column names is
|
|
438
|
+
# supplied, only those columns will be updated. Below is an example:
|
|
439
|
+
#
|
|
440
|
+
# BlogPost.import columns, values, on_duplicate_key_update: :all
|
|
385
441
|
#
|
|
386
442
|
# ==== Using an Array
|
|
387
443
|
#
|
|
@@ -439,7 +495,15 @@ class ActiveRecord::Base
|
|
|
439
495
|
#
|
|
440
496
|
# ===== :columns
|
|
441
497
|
#
|
|
442
|
-
# The :columns attribute can be either an Array or a Hash.
|
|
498
|
+
# The :columns attribute can be either :all, an Array, or a Hash.
|
|
499
|
+
#
|
|
500
|
+
# ===== Using :all
|
|
501
|
+
#
|
|
502
|
+
# The :columns attribute can be :all. All columns other than the primary key will be updated.
|
|
503
|
+
# If a list of column names is supplied, only those columns will be updated.
|
|
504
|
+
# Below is an example:
|
|
505
|
+
#
|
|
506
|
+
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
|
|
443
507
|
#
|
|
444
508
|
# ===== Using an Array
|
|
445
509
|
#
|
|
@@ -474,7 +538,7 @@ class ActiveRecord::Base
|
|
|
474
538
|
import_helper(*args)
|
|
475
539
|
end
|
|
476
540
|
end
|
|
477
|
-
alias import bulk_import unless respond_to? :import
|
|
541
|
+
alias import bulk_import unless ActiveRecord::Base.respond_to? :import
|
|
478
542
|
|
|
479
543
|
# Imports a collection of values if all values are valid. Import fails at the
|
|
480
544
|
# first encountered validation error and raises ActiveRecord::RecordInvalid
|
|
@@ -486,27 +550,17 @@ class ActiveRecord::Base
|
|
|
486
550
|
|
|
487
551
|
bulk_import(*args, options)
|
|
488
552
|
end
|
|
489
|
-
alias import! bulk_import! unless respond_to? :import!
|
|
553
|
+
alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
|
|
490
554
|
|
|
491
555
|
def import_helper( *args )
|
|
492
|
-
options = { validate: true, timestamps: true }
|
|
556
|
+
options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
|
|
493
557
|
options.merge!( args.pop ) if args.last.is_a? Hash
|
|
494
558
|
# making sure that current model's primary key is used
|
|
495
559
|
options[:primary_key] = primary_key
|
|
496
|
-
options[:locking_column] = locking_column if
|
|
497
|
-
|
|
498
|
-
# Don't modify incoming arguments
|
|
499
|
-
on_duplicate_key_update = options[:on_duplicate_key_update]
|
|
500
|
-
if on_duplicate_key_update && on_duplicate_key_update.duplicable?
|
|
501
|
-
options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
|
|
502
|
-
on_duplicate_key_update.each { |k, v| on_duplicate_key_update[k] = v.dup if v.duplicable? }
|
|
503
|
-
else
|
|
504
|
-
on_duplicate_key_update.dup
|
|
505
|
-
end
|
|
506
|
-
end
|
|
560
|
+
options[:locking_column] = locking_column if locking_enabled?
|
|
507
561
|
|
|
508
562
|
is_validating = options[:validate_with_context].present? ? true : options[:validate]
|
|
509
|
-
validator = ActiveRecord::Import::Validator.new(options)
|
|
563
|
+
validator = ActiveRecord::Import::Validator.new(self, options)
|
|
510
564
|
|
|
511
565
|
# assume array of model objects
|
|
512
566
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
|
@@ -522,17 +576,20 @@ class ActiveRecord::Base
|
|
|
522
576
|
end
|
|
523
577
|
end
|
|
524
578
|
|
|
525
|
-
if models.first.id.nil?
|
|
526
|
-
|
|
579
|
+
if models.first.id.nil?
|
|
580
|
+
Array(primary_key).each do |c|
|
|
581
|
+
if column_names.include?(c) && schema_columns_hash[c].type == :uuid
|
|
582
|
+
column_names.delete(c)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
527
585
|
end
|
|
528
586
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
serialized_attributes
|
|
587
|
+
update_attrs = if record_timestamps && options[:timestamps]
|
|
588
|
+
if respond_to?(:timestamp_attributes_for_update, true)
|
|
589
|
+
send(:timestamp_attributes_for_update).map(&:to_sym)
|
|
590
|
+
else
|
|
591
|
+
allocate.send(:timestamp_attributes_for_update_in_model)
|
|
592
|
+
end
|
|
536
593
|
end
|
|
537
594
|
|
|
538
595
|
array_of_attributes = []
|
|
@@ -548,12 +605,12 @@ class ActiveRecord::Base
|
|
|
548
605
|
end
|
|
549
606
|
|
|
550
607
|
array_of_attributes << column_names.map do |name|
|
|
551
|
-
if
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
608
|
+
if model.persisted? &&
|
|
609
|
+
update_attrs && update_attrs.include?(name.to_sym) &&
|
|
610
|
+
!model.send("#{name}_changed?")
|
|
611
|
+
nil
|
|
555
612
|
else
|
|
556
|
-
model.
|
|
613
|
+
model.read_attribute(name.to_s)
|
|
557
614
|
end
|
|
558
615
|
end
|
|
559
616
|
end
|
|
@@ -580,7 +637,7 @@ class ActiveRecord::Base
|
|
|
580
637
|
end
|
|
581
638
|
# supports empty array
|
|
582
639
|
elsif args.last.is_a?( Array ) && args.last.empty?
|
|
583
|
-
return ActiveRecord::Import::Result.new([], 0, [])
|
|
640
|
+
return ActiveRecord::Import::Result.new([], 0, [], [])
|
|
584
641
|
# supports 2-element array and array
|
|
585
642
|
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
|
586
643
|
|
|
@@ -611,17 +668,44 @@ class ActiveRecord::Base
|
|
|
611
668
|
array_of_attributes.each { |a| a.concat(new_fields) }
|
|
612
669
|
end
|
|
613
670
|
|
|
671
|
+
# Don't modify incoming arguments
|
|
672
|
+
on_duplicate_key_update = options[:on_duplicate_key_update]
|
|
673
|
+
if on_duplicate_key_update
|
|
674
|
+
updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
|
|
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
|
|
681
|
+
else
|
|
682
|
+
v
|
|
683
|
+
end
|
|
684
|
+
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
|
+
else
|
|
690
|
+
on_duplicate_key_update
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
614
694
|
timestamps = {}
|
|
615
695
|
|
|
616
696
|
# record timestamps unless disabled in ActiveRecord::Base
|
|
617
|
-
if record_timestamps && options
|
|
697
|
+
if record_timestamps && options[:timestamps]
|
|
618
698
|
timestamps = add_special_rails_stamps column_names, array_of_attributes, options
|
|
619
699
|
end
|
|
620
700
|
|
|
621
701
|
return_obj = if is_validating
|
|
622
702
|
import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
|
|
623
703
|
if models
|
|
624
|
-
models.
|
|
704
|
+
models.each_with_index do |m, i|
|
|
705
|
+
next unless m.errors.any?
|
|
706
|
+
|
|
707
|
+
failed_instances << (options[:track_validation_failures] ? [i, m] : m)
|
|
708
|
+
end
|
|
625
709
|
else
|
|
626
710
|
# create instances for each of our column/value sets
|
|
627
711
|
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
|
@@ -629,14 +713,18 @@ class ActiveRecord::Base
|
|
|
629
713
|
# keep track of the instance and the position it is currently at. if this fails
|
|
630
714
|
# validation we'll use the index to remove it from the array_of_attributes
|
|
631
715
|
arr.each_with_index do |hsh, i|
|
|
632
|
-
|
|
633
|
-
|
|
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
|
+
|
|
634
721
|
next if validator.valid_model?(model)
|
|
635
722
|
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
|
723
|
+
|
|
636
724
|
array_of_attributes[i] = nil
|
|
637
725
|
failure = model.dup
|
|
638
726
|
failure.errors.send(:initialize_dup, model.errors)
|
|
639
|
-
failed_instances << failure
|
|
727
|
+
failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
|
|
640
728
|
end
|
|
641
729
|
array_of_attributes.compact!
|
|
642
730
|
end
|
|
@@ -646,7 +734,7 @@ class ActiveRecord::Base
|
|
|
646
734
|
end
|
|
647
735
|
|
|
648
736
|
if options[:synchronize]
|
|
649
|
-
sync_keys = options[:synchronize_keys] ||
|
|
737
|
+
sync_keys = options[:synchronize_keys] || Array(primary_key)
|
|
650
738
|
synchronize( options[:synchronize], sync_keys)
|
|
651
739
|
end
|
|
652
740
|
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
|
@@ -656,7 +744,10 @@ class ActiveRecord::Base
|
|
|
656
744
|
set_attributes_and_mark_clean(models, return_obj, timestamps, options)
|
|
657
745
|
|
|
658
746
|
# if there are auto-save associations on the models we imported that are new, import them as well
|
|
659
|
-
|
|
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
|
|
660
751
|
end
|
|
661
752
|
|
|
662
753
|
return_obj
|
|
@@ -691,27 +782,29 @@ class ActiveRecord::Base
|
|
|
691
782
|
def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
|
|
692
783
|
return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
|
|
693
784
|
|
|
694
|
-
column_names = column_names.map
|
|
785
|
+
column_names = column_names.map do |name|
|
|
786
|
+
original_name = attribute_alias?(name) ? attribute_alias(name) : name
|
|
787
|
+
original_name.to_sym
|
|
788
|
+
end
|
|
695
789
|
scope_columns, scope_values = scope_attributes.to_a.transpose
|
|
696
790
|
|
|
697
791
|
unless scope_columns.blank?
|
|
698
792
|
scope_columns.zip(scope_values).each do |name, value|
|
|
699
793
|
name_as_sym = name.to_sym
|
|
700
|
-
next if column_names.include?(name_as_sym)
|
|
701
|
-
|
|
702
|
-
is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
|
|
703
|
-
value = Array(value).first if is_sti
|
|
704
|
-
|
|
794
|
+
next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
|
|
705
795
|
column_names << name_as_sym
|
|
706
796
|
array_of_attributes.each { |attrs| attrs << value }
|
|
707
797
|
end
|
|
708
798
|
end
|
|
709
799
|
|
|
710
|
-
|
|
711
|
-
|
|
800
|
+
if finder_needs_type_condition? && !column_names.include?(inheritance_column.to_sym)
|
|
801
|
+
column_names << inheritance_column.to_sym
|
|
802
|
+
array_of_attributes.each { |attrs| attrs << sti_name }
|
|
803
|
+
end
|
|
712
804
|
|
|
805
|
+
columns = column_names.each_with_index.map do |name, i|
|
|
806
|
+
column = schema_columns_hash[name.to_s]
|
|
713
807
|
raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
|
|
714
|
-
|
|
715
808
|
column
|
|
716
809
|
end
|
|
717
810
|
|
|
@@ -727,17 +820,29 @@ class ActiveRecord::Base
|
|
|
727
820
|
if supports_import?
|
|
728
821
|
# generate the sql
|
|
729
822
|
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
|
823
|
+
import_size = values_sql.size
|
|
824
|
+
|
|
825
|
+
batch_size = options[:batch_size] || import_size
|
|
826
|
+
run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
|
|
827
|
+
progress_proc = options[:batch_progress]
|
|
828
|
+
current_batch = 0
|
|
829
|
+
batches = (import_size / batch_size.to_f).ceil
|
|
730
830
|
|
|
731
|
-
batch_size = options[:batch_size] || values_sql.size
|
|
732
831
|
values_sql.each_slice(batch_size) do |batch_values|
|
|
832
|
+
batch_started_at = Time.now.to_i
|
|
833
|
+
|
|
733
834
|
# perform the inserts
|
|
734
835
|
result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
|
|
735
836
|
batch_values,
|
|
736
837
|
options,
|
|
737
|
-
"#{model_name} Create Many
|
|
838
|
+
"#{model_name} Create Many" )
|
|
839
|
+
|
|
738
840
|
number_inserted += result.num_inserts
|
|
739
841
|
ids += result.ids
|
|
740
842
|
results += result.results
|
|
843
|
+
current_batch += 1
|
|
844
|
+
|
|
845
|
+
progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
|
|
741
846
|
end
|
|
742
847
|
else
|
|
743
848
|
transaction(requires_new: true) do
|
|
@@ -752,6 +857,15 @@ class ActiveRecord::Base
|
|
|
752
857
|
|
|
753
858
|
private
|
|
754
859
|
|
|
860
|
+
def associated_options(options, associated_class)
|
|
861
|
+
return options unless options.key?(:recursive_on_duplicate_key_update)
|
|
862
|
+
|
|
863
|
+
table_name = associated_class.arel_table.name.to_sym
|
|
864
|
+
options.merge(
|
|
865
|
+
on_duplicate_key_update: options[:recursive_on_duplicate_key_update][table_name]
|
|
866
|
+
)
|
|
867
|
+
end
|
|
868
|
+
|
|
755
869
|
def set_attributes_and_mark_clean(models, import_result, timestamps, options)
|
|
756
870
|
return if models.nil?
|
|
757
871
|
models -= import_result.failed_instances
|
|
@@ -763,29 +877,56 @@ class ActiveRecord::Base
|
|
|
763
877
|
model.id = id
|
|
764
878
|
|
|
765
879
|
timestamps.each do |attr, value|
|
|
766
|
-
model.send(attr
|
|
880
|
+
model.send("#{attr}=", value) if model.send(attr).nil?
|
|
767
881
|
end
|
|
768
882
|
end
|
|
769
883
|
end
|
|
770
884
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
885
|
+
deserialize_value = lambda do |column, value|
|
|
886
|
+
column = schema_columns_hash[column]
|
|
887
|
+
return value unless column
|
|
888
|
+
if respond_to?(:type_caster)
|
|
889
|
+
type = type_for_attribute(column.name)
|
|
890
|
+
type.deserialize(value)
|
|
891
|
+
elsif column.respond_to?(:type_cast_from_database)
|
|
892
|
+
column.type_cast_from_database(value)
|
|
893
|
+
else
|
|
894
|
+
value
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
set_value = lambda do |model, column, value|
|
|
899
|
+
val = deserialize_value.call(column, value)
|
|
900
|
+
if model.attribute_names.include?(column)
|
|
901
|
+
model.send("#{column}=", val)
|
|
902
|
+
else
|
|
903
|
+
attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
|
|
904
|
+
model.instance_variable_set(:@attributes, attributes)
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
columns = Array(options[:returning_columns])
|
|
909
|
+
results = Array(import_result.results)
|
|
910
|
+
if models.size == results.size
|
|
911
|
+
single_column = columns.first if columns.size == 1
|
|
912
|
+
results.each_with_index do |result, index|
|
|
775
913
|
model = models[index]
|
|
776
914
|
|
|
777
915
|
if single_column
|
|
778
|
-
|
|
916
|
+
set_value.call(model, single_column, result)
|
|
779
917
|
else
|
|
780
918
|
columns.each_with_index do |column, col_index|
|
|
781
|
-
|
|
919
|
+
set_value.call(model, column, result[col_index])
|
|
782
920
|
end
|
|
783
921
|
end
|
|
784
922
|
end
|
|
785
923
|
end
|
|
786
924
|
|
|
787
925
|
models.each do |model|
|
|
788
|
-
if model.respond_to?(:
|
|
926
|
+
if model.respond_to?(:changes_applied) # Rails 4.1.8 and higher
|
|
927
|
+
model.changes_internally_applied if model.respond_to?(:changes_internally_applied) # legacy behavior for Rails 5.1
|
|
928
|
+
model.changes_applied
|
|
929
|
+
elsif model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
|
|
789
930
|
model.clear_changes_information
|
|
790
931
|
else # Rails 3.2
|
|
791
932
|
model.instance_variable_get(:@changed_attributes).clear
|
|
@@ -796,16 +937,22 @@ class ActiveRecord::Base
|
|
|
796
937
|
|
|
797
938
|
# Sync belongs_to association ids with foreign key field
|
|
798
939
|
def load_association_ids(model)
|
|
940
|
+
changed_columns = model.changed
|
|
799
941
|
association_reflections = model.class.reflect_on_all_associations(:belongs_to)
|
|
800
942
|
association_reflections.each do |association_reflection|
|
|
801
|
-
column_name = association_reflection.foreign_key
|
|
802
943
|
next if association_reflection.options[:polymorphic]
|
|
803
|
-
association = model.association(association_reflection.name)
|
|
804
|
-
association = association.target
|
|
805
|
-
next if association.blank? || model.public_send(column_name).present?
|
|
806
944
|
|
|
807
|
-
|
|
808
|
-
|
|
945
|
+
column_names = Array(association_reflection.foreign_key).map(&:to_s)
|
|
946
|
+
column_names.each_with_index do |column_name, column_index|
|
|
947
|
+
next if changed_columns.include?(column_name)
|
|
948
|
+
|
|
949
|
+
association = model.association(association_reflection.name)
|
|
950
|
+
association = association.target
|
|
951
|
+
next if association.blank? || model.public_send(column_name).present?
|
|
952
|
+
|
|
953
|
+
association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
|
|
954
|
+
model.public_send("#{column_name}=", association.send(association_primary_key))
|
|
955
|
+
end
|
|
809
956
|
end
|
|
810
957
|
end
|
|
811
958
|
|
|
@@ -818,16 +965,30 @@ class ActiveRecord::Base
|
|
|
818
965
|
associated_objects_by_class = {}
|
|
819
966
|
models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
|
|
820
967
|
|
|
821
|
-
# :on_duplicate_key_update
|
|
822
|
-
options.delete(:on_duplicate_key_update)
|
|
968
|
+
# :on_duplicate_key_update only supported for all fields
|
|
969
|
+
options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
|
|
970
|
+
# :returning not supported for associations
|
|
971
|
+
options.delete(:returning)
|
|
823
972
|
|
|
824
973
|
associated_objects_by_class.each_value do |associations|
|
|
825
974
|
associations.each_value do |associated_records|
|
|
826
|
-
|
|
975
|
+
next if associated_records.empty?
|
|
976
|
+
|
|
977
|
+
associated_class = associated_records.first.class
|
|
978
|
+
associated_class.bulk_import(associated_records,
|
|
979
|
+
associated_options(options, associated_class))
|
|
827
980
|
end
|
|
828
981
|
end
|
|
829
982
|
end
|
|
830
983
|
|
|
984
|
+
def schema_columns_hash
|
|
985
|
+
if respond_to?(:ignored_columns) && ignored_columns.any?
|
|
986
|
+
connection.schema_cache.columns_hash(table_name)
|
|
987
|
+
else
|
|
988
|
+
columns_hash
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
|
|
831
992
|
# We are eventually going to call Class.import <objects> so we build up a hash
|
|
832
993
|
# of class => objects to import.
|
|
833
994
|
def find_associated_objects_for_import(associated_objects_by_class, model)
|
|
@@ -848,10 +1009,18 @@ class ActiveRecord::Base
|
|
|
848
1009
|
|
|
849
1010
|
changed_objects = association.select { |a| a.new_record? || a.changed? }
|
|
850
1011
|
changed_objects.each do |child|
|
|
851
|
-
|
|
1012
|
+
Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
|
|
1013
|
+
child.public_send("#{column}=", Array(model.id)[index])
|
|
1014
|
+
end
|
|
1015
|
+
|
|
852
1016
|
# For polymorphic associations
|
|
1017
|
+
association_name = if model.class.respond_to?(:polymorphic_name)
|
|
1018
|
+
model.class.polymorphic_name
|
|
1019
|
+
else
|
|
1020
|
+
model.class.base_class
|
|
1021
|
+
end
|
|
853
1022
|
association_reflection.type.try do |type|
|
|
854
|
-
child.public_send("#{type}=",
|
|
1023
|
+
child.public_send("#{type}=", association_name)
|
|
855
1024
|
end
|
|
856
1025
|
end
|
|
857
1026
|
associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
|
|
@@ -871,14 +1040,14 @@ class ActiveRecord::Base
|
|
|
871
1040
|
column = columns[j]
|
|
872
1041
|
|
|
873
1042
|
# be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
|
|
874
|
-
if val.nil? &&
|
|
1043
|
+
if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
|
|
875
1044
|
connection_memo.next_value_for_sequence(sequence_name)
|
|
876
1045
|
elsif val.respond_to?(:to_sql)
|
|
877
1046
|
"(#{val.to_sql})"
|
|
878
1047
|
elsif column
|
|
879
1048
|
if respond_to?(:type_caster) # Rails 5.0 and higher
|
|
880
1049
|
type = type_for_attribute(column.name)
|
|
881
|
-
val = type.type == :boolean ? type.cast(val) : type.serialize(val)
|
|
1050
|
+
val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
|
|
882
1051
|
connection_memo.quote(val)
|
|
883
1052
|
elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
|
|
884
1053
|
connection_memo.quote(column.type_cast_from_user(val), column)
|
|
@@ -887,9 +1056,11 @@ class ActiveRecord::Base
|
|
|
887
1056
|
val = serialized_attributes[column.name].dump(val)
|
|
888
1057
|
end
|
|
889
1058
|
# Fixes #443 to support binary (i.e. bytea) columns on PG
|
|
890
|
-
val = column.type_cast(val) unless column.type.to_sym == :binary
|
|
1059
|
+
val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
|
|
891
1060
|
connection_memo.quote(val, column)
|
|
892
1061
|
end
|
|
1062
|
+
else
|
|
1063
|
+
raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
|
|
893
1064
|
end
|
|
894
1065
|
end
|
|
895
1066
|
"(#{my_values.join(',')})"
|
|
@@ -904,13 +1075,18 @@ class ActiveRecord::Base
|
|
|
904
1075
|
timestamp_columns[:create] = timestamp_attributes_for_create_in_model
|
|
905
1076
|
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
|
906
1077
|
else
|
|
907
|
-
instance =
|
|
1078
|
+
instance = allocate
|
|
908
1079
|
timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
|
|
909
1080
|
timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
|
|
910
1081
|
end
|
|
911
1082
|
|
|
912
1083
|
# use tz as set in ActiveRecord::Base
|
|
913
|
-
|
|
1084
|
+
default_timezone = if ActiveRecord.respond_to?(:default_timezone)
|
|
1085
|
+
ActiveRecord.default_timezone
|
|
1086
|
+
else
|
|
1087
|
+
ActiveRecord::Base.default_timezone
|
|
1088
|
+
end
|
|
1089
|
+
timestamp = default_timezone == :utc ? Time.now.utc : Time.now
|
|
914
1090
|
|
|
915
1091
|
[:create, :update].each do |action|
|
|
916
1092
|
timestamp_columns[action].each do |column|
|
|
@@ -920,7 +1096,7 @@ class ActiveRecord::Base
|
|
|
920
1096
|
index = column_names.index(column) || column_names.index(column.to_sym)
|
|
921
1097
|
if index
|
|
922
1098
|
# replace every instance of the array of attributes with our value
|
|
923
|
-
array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil?
|
|
1099
|
+
array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
|
|
924
1100
|
else
|
|
925
1101
|
column_names << column
|
|
926
1102
|
array_of_attributes.each { |arr| arr << timestamp }
|