activerecord-import 1.0.4 → 2.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/.github/workflows/test.yaml +159 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +76 -7
- data/.rubocop_todo.yml +10 -16
- data/Brewfile +3 -1
- data/CHANGELOG.md +143 -3
- data/Dockerfile +23 -0
- data/Gemfile +28 -24
- data/LICENSE +21 -56
- data/README.markdown +83 -27
- data/Rakefile +3 -0
- data/activerecord-import.gemspec +10 -5
- data/benchmarks/benchmark.rb +10 -6
- data/benchmarks/lib/base.rb +10 -5
- data/benchmarks/lib/cli_parser.rb +10 -6
- 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/5.2.gemfile +2 -0
- data/gemfiles/6.0.gemfile +3 -0
- data/gemfiles/6.1.gemfile +4 -1
- data/gemfiles/7.0.gemfile +4 -0
- data/gemfiles/7.1.gemfile +3 -0
- data/gemfiles/7.2.gemfile +3 -0
- data/gemfiles/8.0.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 +9 -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 +30 -21
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +68 -48
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +37 -30
- data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
- data/lib/activerecord-import/base.rb +3 -1
- data/lib/activerecord-import/import.rb +160 -58
- data/lib/activerecord-import/synchronize.rb +3 -1
- 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 +9 -3
- data/test/import_test.rb +108 -41
- 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 +9 -0
- data/test/models/bike_maker.rb +3 -0
- data/test/models/book.rb +12 -3
- 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 +12 -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 +8 -0
- data/test/models/user.rb +2 -0
- data/test/models/user_token.rb +2 -0
- data/test/models/vendor.rb +2 -0
- data/test/models/widget.rb +12 -3
- 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 +38 -4
- data/test/schema/sqlite3_schema.rb +2 -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 +3 -5
- data/test/support/assertions.rb +2 -0
- data/test/support/factories.rb +2 -0
- data/test/support/generate.rb +4 -2
- data/test/support/mysql/import_examples.rb +7 -8
- data/test/support/postgresql/import_examples.rb +121 -53
- data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +69 -10
- data/test/support/shared_examples/recursive_import.rb +137 -1
- data/test/support/sqlite3/import_examples.rb +2 -1
- data/test/synchronize_test.rb +2 -0
- data/test/test_helper.rb +38 -24
- 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 -22
- data/.travis.yml +0 -74
- data/gemfiles/3.2.gemfile +0 -2
- data/gemfiles/4.0.gemfile +0 -2
- data/gemfiles/4.1.gemfile +0 -2
- data/gemfiles/4.2.gemfile +0 -2
- data/gemfiles/5.0.gemfile +0 -2
- data/gemfiles/5.1.gemfile +0 -2
- data/lib/activerecord-import/mysql2.rb +0 -7
- data/lib/activerecord-import/postgresql.rb +0 -7
- data/lib/activerecord-import/sqlite3.rb +0 -7
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActiveRecord::Import::ConnectionAdapters; end
|
|
4
4
|
|
|
5
|
-
module ActiveRecord::Import
|
|
5
|
+
module ActiveRecord::Import # :nodoc:
|
|
6
6
|
Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
|
|
7
7
|
|
|
8
|
-
module ImportSupport
|
|
9
|
-
def supports_import?
|
|
8
|
+
module ImportSupport # :nodoc:
|
|
9
|
+
def supports_import? # :nodoc:
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module OnDuplicateKeyUpdateSupport # :nodoc:
|
|
15
|
+
def supports_on_duplicate_key_update? # :nodoc:
|
|
10
16
|
true
|
|
11
17
|
end
|
|
12
18
|
end
|
|
@@ -28,7 +34,7 @@ module ActiveRecord::Import #:nodoc:
|
|
|
28
34
|
@validate_callbacks = klass._validate_callbacks.dup
|
|
29
35
|
|
|
30
36
|
@validate_callbacks.each_with_index do |callback, i|
|
|
31
|
-
filter = callback.raw_filter
|
|
37
|
+
filter = callback.respond_to?(:raw_filter) ? callback.raw_filter : callback.filter
|
|
32
38
|
next unless filter.class.name =~ /Validations::PresenceValidator/ ||
|
|
33
39
|
(!@options[:validate_uniqueness] &&
|
|
34
40
|
filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
|
|
@@ -43,17 +49,18 @@ module ActiveRecord::Import #:nodoc:
|
|
|
43
49
|
associations = klass.reflect_on_all_associations(:belongs_to)
|
|
44
50
|
associations.each do |assoc|
|
|
45
51
|
if (index = attrs.index(assoc.name))
|
|
46
|
-
key = assoc.foreign_key.to_sym
|
|
52
|
+
key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
|
|
47
53
|
attrs[index] = key unless attrs.include?(key)
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
end
|
|
51
57
|
|
|
52
|
-
filter.instance_variable_set(:@attributes, attrs)
|
|
58
|
+
filter.instance_variable_set(:@attributes, attrs.flatten)
|
|
53
59
|
|
|
54
60
|
if @validate_callbacks.respond_to?(:chain, true)
|
|
55
61
|
@validate_callbacks.send(:chain).tap do |chain|
|
|
56
62
|
callback.instance_variable_set(:@filter, filter)
|
|
63
|
+
callback.instance_variable_set(:@compiled, nil)
|
|
57
64
|
chain[i] = callback
|
|
58
65
|
end
|
|
59
66
|
else
|
|
@@ -65,7 +72,7 @@ module ActiveRecord::Import #:nodoc:
|
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def valid_model?(model)
|
|
68
|
-
init_validations(model.class) unless model.
|
|
75
|
+
init_validations(model.class) unless model.instance_of?(@validator_class)
|
|
69
76
|
|
|
70
77
|
validation_context = @options[:validate_with_context]
|
|
71
78
|
validation_context ||= (model.new_record? ? :create : :update)
|
|
@@ -77,11 +84,15 @@ module ActiveRecord::Import #:nodoc:
|
|
|
77
84
|
|
|
78
85
|
model.run_callbacks(:validation) do
|
|
79
86
|
if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
|
|
80
|
-
runner = @validate_callbacks.compile
|
|
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
|
|
81
92
|
env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
|
|
82
93
|
if runner.respond_to?(:call) # ActiveRecord < 5.1
|
|
83
94
|
runner.call(env)
|
|
84
|
-
else # ActiveRecord 5.1
|
|
95
|
+
else # ActiveRecord >= 5.1
|
|
85
96
|
# Note that this is a gross simplification of ActiveSupport::Callbacks#run_callbacks.
|
|
86
97
|
# It's technically possible for there to exist an "around" callback in the
|
|
87
98
|
# :validate chain, but this would be an aberration, since Rails doesn't define
|
|
@@ -94,7 +105,8 @@ module ActiveRecord::Import #:nodoc:
|
|
|
94
105
|
# no real-world use case for it.
|
|
95
106
|
raise "The :validate callback chain contains an 'around' callback, which is unsupported" unless runner.final?
|
|
96
107
|
runner.invoke_before(env)
|
|
97
|
-
|
|
108
|
+
# Ensure a truthy value is returned. ActiveRecord < 7.2 always returned an array.
|
|
109
|
+
runner.invoke_after(env) || []
|
|
98
110
|
end
|
|
99
111
|
elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
|
100
112
|
model.instance_eval @validate_callbacks.compile
|
|
@@ -157,7 +169,7 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
157
169
|
m.public_send "#{reflection.type}=", owner.class.name if reflection.type
|
|
158
170
|
end
|
|
159
171
|
|
|
160
|
-
|
|
172
|
+
model_klass.bulk_import column_names, models, options
|
|
161
173
|
|
|
162
174
|
# supports array of hash objects
|
|
163
175
|
elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
|
|
@@ -196,11 +208,11 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
196
208
|
end
|
|
197
209
|
end
|
|
198
210
|
|
|
199
|
-
|
|
211
|
+
model_klass.bulk_import column_names, array_of_attributes, options
|
|
200
212
|
|
|
201
213
|
# supports empty array
|
|
202
214
|
elsif args.last.is_a?( Array ) && args.last.empty?
|
|
203
|
-
|
|
215
|
+
ActiveRecord::Import::Result.new([], 0, [])
|
|
204
216
|
|
|
205
217
|
# supports 2-element array and array
|
|
206
218
|
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
|
@@ -231,7 +243,7 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
|
231
243
|
end
|
|
232
244
|
end
|
|
233
245
|
|
|
234
|
-
|
|
246
|
+
model_klass.bulk_import column_names, array_of_attributes, options
|
|
235
247
|
else
|
|
236
248
|
raise ArgumentError, "Invalid arguments!"
|
|
237
249
|
end
|
|
@@ -241,8 +253,9 @@ end
|
|
|
241
253
|
|
|
242
254
|
module ActiveRecord::Import::Connection
|
|
243
255
|
def establish_connection(args = nil)
|
|
244
|
-
super(args)
|
|
256
|
+
conn = super(args)
|
|
245
257
|
ActiveRecord::Import.load_from_connection_pool connection_pool
|
|
258
|
+
conn
|
|
246
259
|
end
|
|
247
260
|
end
|
|
248
261
|
|
|
@@ -540,11 +553,11 @@ class ActiveRecord::Base
|
|
|
540
553
|
alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
|
|
541
554
|
|
|
542
555
|
def import_helper( *args )
|
|
543
|
-
options = { validate: true, timestamps: true }
|
|
556
|
+
options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
|
|
544
557
|
options.merge!( args.pop ) if args.last.is_a? Hash
|
|
545
558
|
# making sure that current model's primary key is used
|
|
546
559
|
options[:primary_key] = primary_key
|
|
547
|
-
options[:locking_column] = locking_column if
|
|
560
|
+
options[:locking_column] = locking_column if locking_enabled?
|
|
548
561
|
|
|
549
562
|
is_validating = options[:validate_with_context].present? ? true : options[:validate]
|
|
550
563
|
validator = ActiveRecord::Import::Validator.new(self, options)
|
|
@@ -565,7 +578,7 @@ class ActiveRecord::Base
|
|
|
565
578
|
|
|
566
579
|
if models.first.id.nil?
|
|
567
580
|
Array(primary_key).each do |c|
|
|
568
|
-
if column_names.include?(c) &&
|
|
581
|
+
if column_names.include?(c) && schema_columns_hash[c].type == :uuid
|
|
569
582
|
column_names.delete(c)
|
|
570
583
|
end
|
|
571
584
|
end
|
|
@@ -575,7 +588,7 @@ class ActiveRecord::Base
|
|
|
575
588
|
if respond_to?(:timestamp_attributes_for_update, true)
|
|
576
589
|
send(:timestamp_attributes_for_update).map(&:to_sym)
|
|
577
590
|
else
|
|
578
|
-
|
|
591
|
+
allocate.send(:timestamp_attributes_for_update_in_model)
|
|
579
592
|
end
|
|
580
593
|
end
|
|
581
594
|
|
|
@@ -688,7 +701,11 @@ class ActiveRecord::Base
|
|
|
688
701
|
return_obj = if is_validating
|
|
689
702
|
import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
|
|
690
703
|
if models
|
|
691
|
-
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
|
|
692
709
|
else
|
|
693
710
|
# create instances for each of our column/value sets
|
|
694
711
|
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
|
@@ -696,14 +713,18 @@ class ActiveRecord::Base
|
|
|
696
713
|
# keep track of the instance and the position it is currently at. if this fails
|
|
697
714
|
# validation we'll use the index to remove it from the array_of_attributes
|
|
698
715
|
arr.each_with_index do |hsh, i|
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
|
|
701
721
|
next if validator.valid_model?(model)
|
|
702
722
|
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
|
723
|
+
|
|
703
724
|
array_of_attributes[i] = nil
|
|
704
725
|
failure = model.dup
|
|
705
726
|
failure.errors.send(:initialize_dup, model.errors)
|
|
706
|
-
failed_instances << failure
|
|
727
|
+
failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
|
|
707
728
|
end
|
|
708
729
|
array_of_attributes.compact!
|
|
709
730
|
end
|
|
@@ -723,7 +744,10 @@ class ActiveRecord::Base
|
|
|
723
744
|
set_attributes_and_mark_clean(models, return_obj, timestamps, options)
|
|
724
745
|
|
|
725
746
|
# if there are auto-save associations on the models we imported that are new, import them as well
|
|
726
|
-
|
|
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
|
|
727
751
|
end
|
|
728
752
|
|
|
729
753
|
return_obj
|
|
@@ -758,27 +782,29 @@ class ActiveRecord::Base
|
|
|
758
782
|
def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
|
|
759
783
|
return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
|
|
760
784
|
|
|
761
|
-
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
|
|
762
789
|
scope_columns, scope_values = scope_attributes.to_a.transpose
|
|
763
790
|
|
|
764
791
|
unless scope_columns.blank?
|
|
765
792
|
scope_columns.zip(scope_values).each do |name, value|
|
|
766
793
|
name_as_sym = name.to_sym
|
|
767
|
-
next if column_names.include?(name_as_sym)
|
|
768
|
-
|
|
769
|
-
is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
|
|
770
|
-
value = Array(value).first if is_sti
|
|
771
|
-
|
|
794
|
+
next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
|
|
772
795
|
column_names << name_as_sym
|
|
773
796
|
array_of_attributes.each { |attrs| attrs << value }
|
|
774
797
|
end
|
|
775
798
|
end
|
|
776
799
|
|
|
777
|
-
|
|
778
|
-
|
|
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
|
|
779
804
|
|
|
805
|
+
columns = column_names.each_with_index.map do |name, i|
|
|
806
|
+
column = schema_columns_hash[name.to_s]
|
|
780
807
|
raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
|
|
781
|
-
|
|
782
808
|
column
|
|
783
809
|
end
|
|
784
810
|
|
|
@@ -794,17 +820,29 @@ class ActiveRecord::Base
|
|
|
794
820
|
if supports_import?
|
|
795
821
|
# generate the sql
|
|
796
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
|
|
797
830
|
|
|
798
|
-
batch_size = options[:batch_size] || values_sql.size
|
|
799
831
|
values_sql.each_slice(batch_size) do |batch_values|
|
|
832
|
+
batch_started_at = Time.now.to_i
|
|
833
|
+
|
|
800
834
|
# perform the inserts
|
|
801
835
|
result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
|
|
802
836
|
batch_values,
|
|
803
837
|
options,
|
|
804
|
-
"#{model_name} Create Many
|
|
838
|
+
"#{model_name} Create Many" )
|
|
839
|
+
|
|
805
840
|
number_inserted += result.num_inserts
|
|
806
841
|
ids += result.ids
|
|
807
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
|
|
808
846
|
end
|
|
809
847
|
else
|
|
810
848
|
transaction(requires_new: true) do
|
|
@@ -819,6 +857,14 @@ class ActiveRecord::Base
|
|
|
819
857
|
|
|
820
858
|
private
|
|
821
859
|
|
|
860
|
+
def associated_options(options, association)
|
|
861
|
+
return options unless options.key?(:recursive_on_duplicate_key_update)
|
|
862
|
+
|
|
863
|
+
options.merge(
|
|
864
|
+
on_duplicate_key_update: options[:recursive_on_duplicate_key_update][association]
|
|
865
|
+
)
|
|
866
|
+
end
|
|
867
|
+
|
|
822
868
|
def set_attributes_and_mark_clean(models, import_result, timestamps, options)
|
|
823
869
|
return if models.nil?
|
|
824
870
|
models -= import_result.failed_instances
|
|
@@ -830,22 +876,46 @@ class ActiveRecord::Base
|
|
|
830
876
|
model.id = id
|
|
831
877
|
|
|
832
878
|
timestamps.each do |attr, value|
|
|
833
|
-
model.send(attr
|
|
879
|
+
model.send("#{attr}=", value) if model.send(attr).nil?
|
|
834
880
|
end
|
|
835
881
|
end
|
|
836
882
|
end
|
|
837
883
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
884
|
+
deserialize_value = lambda do |column, value|
|
|
885
|
+
column = schema_columns_hash[column]
|
|
886
|
+
return value unless column
|
|
887
|
+
if respond_to?(:type_caster)
|
|
888
|
+
type = type_for_attribute(column.name)
|
|
889
|
+
type.deserialize(value)
|
|
890
|
+
elsif column.respond_to?(:type_cast_from_database)
|
|
891
|
+
column.type_cast_from_database(value)
|
|
892
|
+
else
|
|
893
|
+
value
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
set_value = lambda do |model, column, value|
|
|
898
|
+
val = deserialize_value.call(column, value)
|
|
899
|
+
if model.attribute_names.include?(column)
|
|
900
|
+
model.send("#{column}=", val)
|
|
901
|
+
else
|
|
902
|
+
attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
|
|
903
|
+
model.instance_variable_set(:@attributes, attributes)
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
columns = Array(options[:returning_columns])
|
|
908
|
+
results = Array(import_result.results)
|
|
909
|
+
if models.size == results.size
|
|
910
|
+
single_column = columns.first if columns.size == 1
|
|
911
|
+
results.each_with_index do |result, index|
|
|
842
912
|
model = models[index]
|
|
843
913
|
|
|
844
914
|
if single_column
|
|
845
|
-
|
|
915
|
+
set_value.call(model, single_column, result)
|
|
846
916
|
else
|
|
847
917
|
columns.each_with_index do |column, col_index|
|
|
848
|
-
|
|
918
|
+
set_value.call(model, column, result[col_index])
|
|
849
919
|
end
|
|
850
920
|
end
|
|
851
921
|
end
|
|
@@ -866,16 +936,22 @@ class ActiveRecord::Base
|
|
|
866
936
|
|
|
867
937
|
# Sync belongs_to association ids with foreign key field
|
|
868
938
|
def load_association_ids(model)
|
|
939
|
+
changed_columns = model.changed
|
|
869
940
|
association_reflections = model.class.reflect_on_all_associations(:belongs_to)
|
|
870
941
|
association_reflections.each do |association_reflection|
|
|
871
|
-
column_name = association_reflection.foreign_key
|
|
872
942
|
next if association_reflection.options[:polymorphic]
|
|
873
|
-
association = model.association(association_reflection.name)
|
|
874
|
-
association = association.target
|
|
875
|
-
next if association.blank? || model.public_send(column_name).present?
|
|
876
943
|
|
|
877
|
-
|
|
878
|
-
|
|
944
|
+
column_names = Array(association_reflection.foreign_key).map(&:to_s)
|
|
945
|
+
column_names.each_with_index do |column_name, column_index|
|
|
946
|
+
next if changed_columns.include?(column_name)
|
|
947
|
+
|
|
948
|
+
association = model.association(association_reflection.name)
|
|
949
|
+
association = association.target
|
|
950
|
+
next if association.blank? || model.public_send(column_name).present?
|
|
951
|
+
|
|
952
|
+
association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
|
|
953
|
+
model.public_send("#{column_name}=", association.send(association_primary_key))
|
|
954
|
+
end
|
|
879
955
|
end
|
|
880
956
|
end
|
|
881
957
|
|
|
@@ -888,17 +964,30 @@ class ActiveRecord::Base
|
|
|
888
964
|
associated_objects_by_class = {}
|
|
889
965
|
models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
|
|
890
966
|
|
|
891
|
-
# :on_duplicate_key_update
|
|
892
|
-
options.delete(:on_duplicate_key_update)
|
|
967
|
+
# :on_duplicate_key_update only supported for all fields
|
|
968
|
+
options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
|
|
969
|
+
# :returning not supported for associations
|
|
893
970
|
options.delete(:returning)
|
|
894
971
|
|
|
895
972
|
associated_objects_by_class.each_value do |associations|
|
|
896
|
-
associations.
|
|
897
|
-
|
|
973
|
+
associations.each do |association, associated_records|
|
|
974
|
+
next if associated_records.empty?
|
|
975
|
+
|
|
976
|
+
associated_class = associated_records.first.class
|
|
977
|
+
associated_class.bulk_import(associated_records,
|
|
978
|
+
associated_options(options, association))
|
|
898
979
|
end
|
|
899
980
|
end
|
|
900
981
|
end
|
|
901
982
|
|
|
983
|
+
def schema_columns_hash
|
|
984
|
+
if respond_to?(:ignored_columns) && ignored_columns.any?
|
|
985
|
+
connection.schema_cache.columns_hash(table_name)
|
|
986
|
+
else
|
|
987
|
+
columns_hash
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
|
|
902
991
|
# We are eventually going to call Class.import <objects> so we build up a hash
|
|
903
992
|
# of class => objects to import.
|
|
904
993
|
def find_associated_objects_for_import(associated_objects_by_class, model)
|
|
@@ -919,10 +1008,18 @@ class ActiveRecord::Base
|
|
|
919
1008
|
|
|
920
1009
|
changed_objects = association.select { |a| a.new_record? || a.changed? }
|
|
921
1010
|
changed_objects.each do |child|
|
|
922
|
-
|
|
1011
|
+
Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
|
|
1012
|
+
child.public_send("#{column}=", Array(model.id)[index])
|
|
1013
|
+
end
|
|
1014
|
+
|
|
923
1015
|
# For polymorphic associations
|
|
1016
|
+
association_name = if model.class.respond_to?(:polymorphic_name)
|
|
1017
|
+
model.class.polymorphic_name
|
|
1018
|
+
else
|
|
1019
|
+
model.class.base_class
|
|
1020
|
+
end
|
|
924
1021
|
association_reflection.type.try do |type|
|
|
925
|
-
child.public_send("#{type}=",
|
|
1022
|
+
child.public_send("#{type}=", association_name)
|
|
926
1023
|
end
|
|
927
1024
|
end
|
|
928
1025
|
associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
|
|
@@ -949,7 +1046,7 @@ class ActiveRecord::Base
|
|
|
949
1046
|
elsif column
|
|
950
1047
|
if respond_to?(:type_caster) # Rails 5.0 and higher
|
|
951
1048
|
type = type_for_attribute(column.name)
|
|
952
|
-
val = type.type == :boolean ? type.cast(val) : type.serialize(val)
|
|
1049
|
+
val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
|
|
953
1050
|
connection_memo.quote(val)
|
|
954
1051
|
elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
|
|
955
1052
|
connection_memo.quote(column.type_cast_from_user(val), column)
|
|
@@ -977,13 +1074,18 @@ class ActiveRecord::Base
|
|
|
977
1074
|
timestamp_columns[:create] = timestamp_attributes_for_create_in_model
|
|
978
1075
|
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
|
979
1076
|
else
|
|
980
|
-
instance =
|
|
1077
|
+
instance = allocate
|
|
981
1078
|
timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
|
|
982
1079
|
timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
|
|
983
1080
|
end
|
|
984
1081
|
|
|
985
1082
|
# use tz as set in ActiveRecord::Base
|
|
986
|
-
|
|
1083
|
+
default_timezone = if ActiveRecord.respond_to?(:default_timezone)
|
|
1084
|
+
ActiveRecord.default_timezone
|
|
1085
|
+
else
|
|
1086
|
+
ActiveRecord::Base.default_timezone
|
|
1087
|
+
end
|
|
1088
|
+
timestamp = default_timezone == :utc ? Time.now.utc : Time.now
|
|
987
1089
|
|
|
988
1090
|
[:create, :update].each do |action|
|
|
989
1091
|
timestamp_columns[action].each do |column|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveRecord # :nodoc:
|
|
2
4
|
class Base # :nodoc:
|
|
3
5
|
# Synchronizes the passed in ActiveRecord instances with data
|
|
@@ -39,7 +41,7 @@ module ActiveRecord # :nodoc:
|
|
|
39
41
|
|
|
40
42
|
next unless matched_instance
|
|
41
43
|
|
|
42
|
-
instance.
|
|
44
|
+
instance.instance_variable_set :@association_cache, {}
|
|
43
45
|
instance.send :clear_aggregation_cache if instance.respond_to?(:clear_aggregation_cache, true)
|
|
44
46
|
instance.instance_variable_set :@attributes, matched_instance.instance_variable_get(:@attributes)
|
|
45
47
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/array'
|
|
4
|
+
|
|
1
5
|
module ActiveRecord::Import
|
|
2
6
|
class ValueSetTooLargeError < StandardError
|
|
3
7
|
attr_reader :size
|
|
8
|
+
|
|
4
9
|
def initialize(msg = "Value set exceeds max size", size = 0)
|
|
5
10
|
@size = size
|
|
6
11
|
super(msg)
|
data/lib/activerecord-import.rb
CHANGED
data/test/adapters/jdbcmysql.rb
CHANGED
data/test/adapters/mysql2.rb
CHANGED
data/test/adapters/postgis.rb
CHANGED
data/test/adapters/postgresql.rb
CHANGED
data/test/adapters/spatialite.rb
CHANGED
data/test/adapters/sqlite3.rb
CHANGED
data/test/database.yml.sample
CHANGED
|
@@ -8,6 +8,7 @@ common: &common
|
|
|
8
8
|
mysql2: &mysql2
|
|
9
9
|
<<: *common
|
|
10
10
|
adapter: mysql2
|
|
11
|
+
host: mysql
|
|
11
12
|
|
|
12
13
|
mysql2spatial:
|
|
13
14
|
<<: *mysql2
|
|
@@ -19,6 +20,7 @@ postgresql: &postgresql
|
|
|
19
20
|
<<: *common
|
|
20
21
|
username: postgres
|
|
21
22
|
adapter: postgresql
|
|
23
|
+
host: postgresql
|
|
22
24
|
min_messages: warning
|
|
23
25
|
|
|
24
26
|
postresql_makara:
|
|
@@ -50,3 +52,8 @@ sqlite3: &sqlite3
|
|
|
50
52
|
|
|
51
53
|
spatialite:
|
|
52
54
|
<<: *sqlite3
|
|
55
|
+
|
|
56
|
+
trilogy:
|
|
57
|
+
<<: *common
|
|
58
|
+
adapter: trilogy
|
|
59
|
+
host: mysql
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
common: &common
|
|
2
2
|
username: root
|
|
3
|
-
password:
|
|
3
|
+
password: root
|
|
4
4
|
encoding: utf8
|
|
5
|
-
|
|
5
|
+
collation: utf8_general_ci
|
|
6
|
+
host: 127.0.0.1
|
|
6
7
|
database: activerecord_import_test
|
|
7
8
|
|
|
8
9
|
jdbcpostgresql: &postgresql
|
|
@@ -37,6 +38,7 @@ oracle:
|
|
|
37
38
|
postgresql: &postgresql
|
|
38
39
|
<<: *common
|
|
39
40
|
username: postgres
|
|
41
|
+
password: postgres
|
|
40
42
|
adapter: postgresql
|
|
41
43
|
min_messages: warning
|
|
42
44
|
|
|
@@ -52,7 +54,7 @@ seamless_database_pool:
|
|
|
52
54
|
pool_adapter: mysql2
|
|
53
55
|
prepared_statements: false
|
|
54
56
|
master:
|
|
55
|
-
host:
|
|
57
|
+
host: 127.0.0.1
|
|
56
58
|
|
|
57
59
|
sqlite:
|
|
58
60
|
adapter: sqlite
|
|
@@ -64,3 +66,7 @@ sqlite3: &sqlite3
|
|
|
64
66
|
|
|
65
67
|
spatialite:
|
|
66
68
|
<<: *sqlite3
|
|
69
|
+
|
|
70
|
+
trilogy:
|
|
71
|
+
<<: *common
|
|
72
|
+
adapter: trilogy
|