activerecord-import 1.0.2 → 1.3.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 +78 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +91 -3
- data/Gemfile +6 -2
- data/LICENSE +21 -56
- data/README.markdown +61 -53
- data/activerecord-import.gemspec +4 -4
- data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
- data/gemfiles/6.0.gemfile +2 -1
- data/gemfiles/6.1.gemfile +2 -1
- data/gemfiles/7.0.gemfile +1 -0
- data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +4 -4
- data/lib/activerecord-import/adapters/abstract_adapter.rb +6 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +6 -6
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +3 -11
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -15
- data/lib/activerecord-import/base.rb +7 -1
- data/lib/activerecord-import/import.rb +95 -42
- data/lib/activerecord-import/synchronize.rb +1 -1
- data/lib/activerecord-import/value_sets_parser.rb +2 -0
- data/lib/activerecord-import/version.rb +1 -1
- data/test/{travis → github}/database.yml +3 -1
- data/test/import_test.rb +67 -1
- data/test/models/animal.rb +6 -0
- data/test/models/card.rb +3 -0
- data/test/models/customer.rb +6 -0
- data/test/models/deck.rb +6 -0
- data/test/models/order.rb +6 -0
- data/test/models/playing_card.rb +2 -0
- data/test/schema/generic_schema.rb +25 -0
- data/test/schema/postgresql_schema.rb +14 -0
- data/test/support/postgresql/import_examples.rb +55 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +10 -0
- data/test/support/shared_examples/recursive_import.rb +30 -1
- data/test/test_helper.rb +10 -1
- metadata +25 -16
- data/.travis.yml +0 -70
- data/gemfiles/3.2.gemfile +0 -2
- data/gemfiles/4.0.gemfile +0 -2
- data/gemfiles/4.1.gemfile +0 -2
@@ -1,6 +1,6 @@
|
|
1
|
-
require "active_record/connection_adapters/
|
2
|
-
require "activerecord-import/adapters/
|
1
|
+
require "active_record/connection_adapters/mysql2_adapter"
|
2
|
+
require "activerecord-import/adapters/mysql2_adapter"
|
3
3
|
|
4
|
-
class ActiveRecord::ConnectionAdapters::
|
5
|
-
include ActiveRecord::Import::
|
4
|
+
class ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
5
|
+
include ActiveRecord::Import::Mysql2Adapter
|
6
6
|
end
|
@@ -59,6 +59,12 @@ module ActiveRecord::Import::AbstractAdapter
|
|
59
59
|
post_sql_statements
|
60
60
|
end
|
61
61
|
|
62
|
+
def increment_locking_column!(table_name, results, locking_column)
|
63
|
+
if locking_column.present?
|
64
|
+
results << "\"#{locking_column}\"=#{table_name}.\"#{locking_column}\"+1"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
62
68
|
def supports_on_duplicate_key_update?
|
63
69
|
false
|
64
70
|
end
|
@@ -56,9 +56,9 @@ module ActiveRecord::Import::MysqlAdapter
|
|
56
56
|
# in a single packet
|
57
57
|
def max_allowed_packet # :nodoc:
|
58
58
|
@max_allowed_packet ||= begin
|
59
|
-
result = execute( "
|
59
|
+
result = execute( "SELECT @@max_allowed_packet" )
|
60
60
|
# original Mysql gem responds to #fetch_row while Mysql2 responds to #first
|
61
|
-
val = result.respond_to?(:fetch_row) ? result.fetch_row[
|
61
|
+
val = result.respond_to?(:fetch_row) ? result.fetch_row[0] : result.first[0]
|
62
62
|
val.to_i
|
63
63
|
end
|
64
64
|
end
|
@@ -102,7 +102,7 @@ module ActiveRecord::Import::MysqlAdapter
|
|
102
102
|
qc = quote_column_name( column )
|
103
103
|
"#{table_name}.#{qc}=VALUES(#{qc})"
|
104
104
|
end
|
105
|
-
increment_locking_column!(
|
105
|
+
increment_locking_column!(table_name, results, locking_column)
|
106
106
|
results.join( ',' )
|
107
107
|
end
|
108
108
|
|
@@ -112,7 +112,7 @@ module ActiveRecord::Import::MysqlAdapter
|
|
112
112
|
qc2 = quote_column_name( column2 )
|
113
113
|
"#{table_name}.#{qc1}=VALUES( #{qc2} )"
|
114
114
|
end
|
115
|
-
increment_locking_column!(
|
115
|
+
increment_locking_column!(table_name, results, locking_column)
|
116
116
|
results.join( ',')
|
117
117
|
end
|
118
118
|
|
@@ -121,9 +121,9 @@ module ActiveRecord::Import::MysqlAdapter
|
|
121
121
|
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
|
122
122
|
end
|
123
123
|
|
124
|
-
def increment_locking_column!(
|
124
|
+
def increment_locking_column!(table_name, results, locking_column)
|
125
125
|
if locking_column.present?
|
126
|
-
results << "
|
126
|
+
results << "`#{locking_column}`=#{table_name}.`#{locking_column}`+1"
|
127
127
|
end
|
128
128
|
end
|
129
129
|
end
|
@@ -28,7 +28,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
28
28
|
else
|
29
29
|
select_values( sql2insert, *args )
|
30
30
|
end
|
31
|
-
|
31
|
+
clear_query_cache if query_cache_enabled
|
32
32
|
end
|
33
33
|
|
34
34
|
if options[:returning].blank?
|
@@ -158,7 +158,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
158
158
|
qc = quote_column_name( column )
|
159
159
|
"#{qc}=EXCLUDED.#{qc}"
|
160
160
|
end
|
161
|
-
increment_locking_column!(results, locking_column)
|
161
|
+
increment_locking_column!(table_name, results, locking_column)
|
162
162
|
results.join( ',' )
|
163
163
|
end
|
164
164
|
|
@@ -168,7 +168,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
168
168
|
qc2 = quote_column_name( column2 )
|
169
169
|
"#{qc1}=EXCLUDED.#{qc2}"
|
170
170
|
end
|
171
|
-
increment_locking_column!(results, locking_column)
|
171
|
+
increment_locking_column!(table_name, results, locking_column)
|
172
172
|
results.join( ',' )
|
173
173
|
end
|
174
174
|
|
@@ -203,14 +203,6 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
203
203
|
true
|
204
204
|
end
|
205
205
|
|
206
|
-
def increment_locking_column!(results, locking_column)
|
207
|
-
if locking_column.present?
|
208
|
-
results << "\"#{locking_column}\"=EXCLUDED.\"#{locking_column}\"+1"
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
private
|
213
|
-
|
214
206
|
def database_version
|
215
207
|
defined?(postgresql_version) ? postgresql_version : super
|
216
208
|
end
|
@@ -92,7 +92,7 @@ module ActiveRecord::Import::SQLite3Adapter
|
|
92
92
|
|
93
93
|
# Returns a generated ON CONFLICT DO UPDATE statement given the passed
|
94
94
|
# in +args+.
|
95
|
-
def sql_for_on_duplicate_key_update(
|
95
|
+
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
96
96
|
arg, primary_key, locking_column = args
|
97
97
|
arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
|
98
98
|
return unless arg.is_a?( Hash )
|
@@ -113,9 +113,9 @@ module ActiveRecord::Import::SQLite3Adapter
|
|
113
113
|
|
114
114
|
sql << "#{conflict_target}DO UPDATE SET "
|
115
115
|
if columns.is_a?( Array )
|
116
|
-
sql << sql_for_on_duplicate_key_update_as_array( locking_column, columns )
|
116
|
+
sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
|
117
117
|
elsif columns.is_a?( Hash )
|
118
|
-
sql << sql_for_on_duplicate_key_update_as_hash( locking_column, columns )
|
118
|
+
sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
|
119
119
|
elsif columns.is_a?( String )
|
120
120
|
sql << columns
|
121
121
|
else
|
@@ -127,22 +127,22 @@ module ActiveRecord::Import::SQLite3Adapter
|
|
127
127
|
sql
|
128
128
|
end
|
129
129
|
|
130
|
-
def sql_for_on_duplicate_key_update_as_array( locking_column, arr ) # :nodoc:
|
130
|
+
def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
|
131
131
|
results = arr.map do |column|
|
132
132
|
qc = quote_column_name( column )
|
133
133
|
"#{qc}=EXCLUDED.#{qc}"
|
134
134
|
end
|
135
|
-
increment_locking_column!(results, locking_column)
|
135
|
+
increment_locking_column!(table_name, results, locking_column)
|
136
136
|
results.join( ',' )
|
137
137
|
end
|
138
138
|
|
139
|
-
def sql_for_on_duplicate_key_update_as_hash( locking_column, hsh ) # :nodoc:
|
139
|
+
def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
|
140
140
|
results = hsh.map do |column1, column2|
|
141
141
|
qc1 = quote_column_name( column1 )
|
142
142
|
qc2 = quote_column_name( column2 )
|
143
143
|
"#{qc1}=EXCLUDED.#{qc2}"
|
144
144
|
end
|
145
|
-
increment_locking_column!(results, locking_column)
|
145
|
+
increment_locking_column!(table_name, results, locking_column)
|
146
146
|
results.join( ',' )
|
147
147
|
end
|
148
148
|
|
@@ -166,14 +166,6 @@ module ActiveRecord::Import::SQLite3Adapter
|
|
166
166
|
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
|
167
167
|
end
|
168
168
|
|
169
|
-
def increment_locking_column!(results, locking_column)
|
170
|
-
if locking_column.present?
|
171
|
-
results << "\"#{locking_column}\"=EXCLUDED.\"#{locking_column}\"+1"
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
private
|
176
|
-
|
177
169
|
def database_version
|
178
170
|
defined?(sqlite_version) ? sqlite_version : super
|
179
171
|
end
|
@@ -27,7 +27,13 @@ module ActiveRecord::Import
|
|
27
27
|
|
28
28
|
# Loads the import functionality for the passed in ActiveRecord connection
|
29
29
|
def self.load_from_connection_pool(connection_pool)
|
30
|
-
|
30
|
+
adapter =
|
31
|
+
if connection_pool.respond_to?(:db_config) # ActiveRecord >= 6.1
|
32
|
+
connection_pool.db_config.adapter
|
33
|
+
else
|
34
|
+
connection_pool.spec.config[:adapter]
|
35
|
+
end
|
36
|
+
require_adapter adapter
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
@@ -34,7 +34,7 @@ module ActiveRecord::Import #:nodoc:
|
|
34
34
|
@validate_callbacks = klass._validate_callbacks.dup
|
35
35
|
|
36
36
|
@validate_callbacks.each_with_index do |callback, i|
|
37
|
-
filter = callback.raw_filter
|
37
|
+
filter = callback.respond_to?(:raw_filter) ? callback.raw_filter : callback.filter
|
38
38
|
next unless filter.class.name =~ /Validations::PresenceValidator/ ||
|
39
39
|
(!@options[:validate_uniqueness] &&
|
40
40
|
filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
|
@@ -49,7 +49,7 @@ module ActiveRecord::Import #:nodoc:
|
|
49
49
|
associations = klass.reflect_on_all_associations(:belongs_to)
|
50
50
|
associations.each do |assoc|
|
51
51
|
if (index = attrs.index(assoc.name))
|
52
|
-
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
|
53
53
|
attrs[index] = key unless attrs.include?(key)
|
54
54
|
end
|
55
55
|
end
|
@@ -245,16 +245,17 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
245
245
|
alias import bulk_import unless respond_to? :import
|
246
246
|
end
|
247
247
|
|
248
|
+
module ActiveRecord::Import::Connection
|
249
|
+
def establish_connection(args = nil)
|
250
|
+
conn = super(args)
|
251
|
+
ActiveRecord::Import.load_from_connection_pool connection_pool
|
252
|
+
conn
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
248
256
|
class ActiveRecord::Base
|
249
257
|
class << self
|
250
|
-
|
251
|
-
conn = establish_connection_without_activerecord_import(*args)
|
252
|
-
ActiveRecord::Import.load_from_connection_pool connection_pool
|
253
|
-
conn
|
254
|
-
end
|
255
|
-
|
256
|
-
alias establish_connection_without_activerecord_import establish_connection
|
257
|
-
alias establish_connection establish_connection_with_activerecord_import
|
258
|
+
prepend ActiveRecord::Import::Connection
|
258
259
|
|
259
260
|
# Returns true if the current database connection adapter
|
260
261
|
# supports import functionality, otherwise returns false.
|
@@ -546,7 +547,7 @@ class ActiveRecord::Base
|
|
546
547
|
alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
|
547
548
|
|
548
549
|
def import_helper( *args )
|
549
|
-
options = { validate: true, timestamps: true }
|
550
|
+
options = { validate: true, timestamps: true, track_validation_failures: false }
|
550
551
|
options.merge!( args.pop ) if args.last.is_a? Hash
|
551
552
|
# making sure that current model's primary key is used
|
552
553
|
options[:primary_key] = primary_key
|
@@ -581,7 +582,7 @@ class ActiveRecord::Base
|
|
581
582
|
if respond_to?(:timestamp_attributes_for_update, true)
|
582
583
|
send(:timestamp_attributes_for_update).map(&:to_sym)
|
583
584
|
else
|
584
|
-
|
585
|
+
allocate.send(:timestamp_attributes_for_update_in_model)
|
585
586
|
end
|
586
587
|
end
|
587
588
|
|
@@ -630,7 +631,7 @@ class ActiveRecord::Base
|
|
630
631
|
end
|
631
632
|
# supports empty array
|
632
633
|
elsif args.last.is_a?( Array ) && args.last.empty?
|
633
|
-
return ActiveRecord::Import::Result.new([], 0, [])
|
634
|
+
return ActiveRecord::Import::Result.new([], 0, [], [])
|
634
635
|
# supports 2-element array and array
|
635
636
|
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
636
637
|
|
@@ -702,14 +703,18 @@ class ActiveRecord::Base
|
|
702
703
|
# keep track of the instance and the position it is currently at. if this fails
|
703
704
|
# validation we'll use the index to remove it from the array_of_attributes
|
704
705
|
arr.each_with_index do |hsh, i|
|
705
|
-
|
706
|
-
|
706
|
+
# utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
|
707
|
+
model = new do |m|
|
708
|
+
hsh.each_pair { |k, v| m[k] = v }
|
709
|
+
end
|
710
|
+
|
707
711
|
next if validator.valid_model?(model)
|
708
712
|
raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
|
713
|
+
|
709
714
|
array_of_attributes[i] = nil
|
710
715
|
failure = model.dup
|
711
716
|
failure.errors.send(:initialize_dup, model.errors)
|
712
|
-
failed_instances << failure
|
717
|
+
failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
|
713
718
|
end
|
714
719
|
array_of_attributes.compact!
|
715
720
|
end
|
@@ -729,7 +734,10 @@ class ActiveRecord::Base
|
|
729
734
|
set_attributes_and_mark_clean(models, return_obj, timestamps, options)
|
730
735
|
|
731
736
|
# if there are auto-save associations on the models we imported that are new, import them as well
|
732
|
-
|
737
|
+
if options[:recursive]
|
738
|
+
options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
|
739
|
+
import_associations(models, options.dup.merge(validate: false))
|
740
|
+
end
|
733
741
|
end
|
734
742
|
|
735
743
|
return_obj
|
@@ -770,21 +778,22 @@ class ActiveRecord::Base
|
|
770
778
|
unless scope_columns.blank?
|
771
779
|
scope_columns.zip(scope_values).each do |name, value|
|
772
780
|
name_as_sym = name.to_sym
|
773
|
-
next if column_names.include?(name_as_sym)
|
774
|
-
|
775
|
-
is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
|
776
|
-
value = Array(value).first if is_sti
|
777
|
-
|
781
|
+
next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
|
778
782
|
column_names << name_as_sym
|
779
783
|
array_of_attributes.each { |attrs| attrs << value }
|
780
784
|
end
|
781
785
|
end
|
782
786
|
|
787
|
+
if finder_needs_type_condition?
|
788
|
+
unless column_names.include?(inheritance_column.to_sym)
|
789
|
+
column_names << inheritance_column.to_sym
|
790
|
+
array_of_attributes.each { |attrs| attrs << sti_name }
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
783
794
|
columns = column_names.each_with_index.map do |name, i|
|
784
795
|
column = columns_hash[name.to_s]
|
785
|
-
|
786
796
|
raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
|
787
|
-
|
788
797
|
column
|
789
798
|
end
|
790
799
|
|
@@ -800,17 +809,29 @@ class ActiveRecord::Base
|
|
800
809
|
if supports_import?
|
801
810
|
# generate the sql
|
802
811
|
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
812
|
+
import_size = values_sql.size
|
813
|
+
|
814
|
+
batch_size = options[:batch_size] || import_size
|
815
|
+
run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
|
816
|
+
progress_proc = options[:batch_progress]
|
817
|
+
current_batch = 0
|
818
|
+
batches = (import_size / batch_size.to_f).ceil
|
803
819
|
|
804
|
-
batch_size = options[:batch_size] || values_sql.size
|
805
820
|
values_sql.each_slice(batch_size) do |batch_values|
|
821
|
+
batch_started_at = Time.now.to_i
|
822
|
+
|
806
823
|
# perform the inserts
|
807
824
|
result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
|
808
825
|
batch_values,
|
809
826
|
options,
|
810
|
-
"#{model_name} Create Many
|
827
|
+
"#{model_name} Create Many" )
|
828
|
+
|
811
829
|
number_inserted += result.num_inserts
|
812
830
|
ids += result.ids
|
813
831
|
results += result.results
|
832
|
+
current_batch += 1
|
833
|
+
|
834
|
+
progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
|
814
835
|
end
|
815
836
|
else
|
816
837
|
transaction(requires_new: true) do
|
@@ -836,11 +857,24 @@ class ActiveRecord::Base
|
|
836
857
|
model.id = id
|
837
858
|
|
838
859
|
timestamps.each do |attr, value|
|
839
|
-
model.send(attr + "=", value)
|
860
|
+
model.send(attr + "=", value) if model.send(attr).nil?
|
840
861
|
end
|
841
862
|
end
|
842
863
|
end
|
843
864
|
|
865
|
+
deserialize_value = lambda do |column, value|
|
866
|
+
column = columns_hash[column]
|
867
|
+
return value unless column
|
868
|
+
if respond_to?(:type_caster)
|
869
|
+
type = type_for_attribute(column.name)
|
870
|
+
type.deserialize(value)
|
871
|
+
elsif column.respond_to?(:type_cast_from_database)
|
872
|
+
column.type_cast_from_database(value)
|
873
|
+
else
|
874
|
+
value
|
875
|
+
end
|
876
|
+
end
|
877
|
+
|
844
878
|
if models.size == import_result.results.size
|
845
879
|
columns = Array(options[:returning])
|
846
880
|
single_column = "#{columns.first}=" if columns.size == 1
|
@@ -848,10 +882,12 @@ class ActiveRecord::Base
|
|
848
882
|
model = models[index]
|
849
883
|
|
850
884
|
if single_column
|
851
|
-
|
885
|
+
val = deserialize_value.call(columns.first, result)
|
886
|
+
model.send(single_column, val)
|
852
887
|
else
|
853
888
|
columns.each_with_index do |column, col_index|
|
854
|
-
|
889
|
+
val = deserialize_value.call(column, result[col_index])
|
890
|
+
model.send("#{column}=", val)
|
855
891
|
end
|
856
892
|
end
|
857
893
|
end
|
@@ -872,16 +908,22 @@ class ActiveRecord::Base
|
|
872
908
|
|
873
909
|
# Sync belongs_to association ids with foreign key field
|
874
910
|
def load_association_ids(model)
|
911
|
+
changed_columns = model.changed
|
875
912
|
association_reflections = model.class.reflect_on_all_associations(:belongs_to)
|
876
913
|
association_reflections.each do |association_reflection|
|
877
|
-
column_name = association_reflection.foreign_key
|
878
914
|
next if association_reflection.options[:polymorphic]
|
879
|
-
association = model.association(association_reflection.name)
|
880
|
-
association = association.target
|
881
|
-
next if association.blank? || model.public_send(column_name).present?
|
882
915
|
|
883
|
-
|
884
|
-
|
916
|
+
column_names = Array(association_reflection.foreign_key).map(&:to_s)
|
917
|
+
column_names.each_with_index do |column_name, column_index|
|
918
|
+
next if changed_columns.include?(column_name)
|
919
|
+
|
920
|
+
association = model.association(association_reflection.name)
|
921
|
+
association = association.target
|
922
|
+
next if association.blank? || model.public_send(column_name).present?
|
923
|
+
|
924
|
+
association_primary_key = Array(association_reflection.association_primary_key)[column_index]
|
925
|
+
model.public_send("#{column_name}=", association.send(association_primary_key))
|
926
|
+
end
|
885
927
|
end
|
886
928
|
end
|
887
929
|
|
@@ -894,8 +936,9 @@ class ActiveRecord::Base
|
|
894
936
|
associated_objects_by_class = {}
|
895
937
|
models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
|
896
938
|
|
897
|
-
# :on_duplicate_key_update
|
898
|
-
options.delete(:on_duplicate_key_update)
|
939
|
+
# :on_duplicate_key_update only supported for all fields
|
940
|
+
options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
|
941
|
+
# :returning not supported for associations
|
899
942
|
options.delete(:returning)
|
900
943
|
|
901
944
|
associated_objects_by_class.each_value do |associations|
|
@@ -927,8 +970,13 @@ class ActiveRecord::Base
|
|
927
970
|
changed_objects.each do |child|
|
928
971
|
child.public_send("#{association_reflection.foreign_key}=", model.id)
|
929
972
|
# For polymorphic associations
|
973
|
+
association_name = if model.class.respond_to?(:polymorphic_name)
|
974
|
+
model.class.polymorphic_name
|
975
|
+
else
|
976
|
+
model.class.base_class
|
977
|
+
end
|
930
978
|
association_reflection.type.try do |type|
|
931
|
-
child.public_send("#{type}=",
|
979
|
+
child.public_send("#{type}=", association_name)
|
932
980
|
end
|
933
981
|
end
|
934
982
|
associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
|
@@ -955,7 +1003,7 @@ class ActiveRecord::Base
|
|
955
1003
|
elsif column
|
956
1004
|
if respond_to?(:type_caster) # Rails 5.0 and higher
|
957
1005
|
type = type_for_attribute(column.name)
|
958
|
-
val = type.type == :boolean ? type.cast(val) : type.serialize(val)
|
1006
|
+
val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
|
959
1007
|
connection_memo.quote(val)
|
960
1008
|
elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
|
961
1009
|
connection_memo.quote(column.type_cast_from_user(val), column)
|
@@ -964,7 +1012,7 @@ class ActiveRecord::Base
|
|
964
1012
|
val = serialized_attributes[column.name].dump(val)
|
965
1013
|
end
|
966
1014
|
# Fixes #443 to support binary (i.e. bytea) columns on PG
|
967
|
-
val = column.type_cast(val) unless column.type.to_sym == :binary
|
1015
|
+
val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
|
968
1016
|
connection_memo.quote(val, column)
|
969
1017
|
end
|
970
1018
|
else
|
@@ -983,13 +1031,18 @@ class ActiveRecord::Base
|
|
983
1031
|
timestamp_columns[:create] = timestamp_attributes_for_create_in_model
|
984
1032
|
timestamp_columns[:update] = timestamp_attributes_for_update_in_model
|
985
1033
|
else
|
986
|
-
instance =
|
1034
|
+
instance = allocate
|
987
1035
|
timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
|
988
1036
|
timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
|
989
1037
|
end
|
990
1038
|
|
991
1039
|
# use tz as set in ActiveRecord::Base
|
992
|
-
|
1040
|
+
default_timezone = if ActiveRecord.respond_to?(:default_timezone)
|
1041
|
+
ActiveRecord.default_timezone
|
1042
|
+
else
|
1043
|
+
ActiveRecord::Base.default_timezone
|
1044
|
+
end
|
1045
|
+
timestamp = default_timezone == :utc ? Time.now.utc : Time.now
|
993
1046
|
|
994
1047
|
[:create, :update].each do |action|
|
995
1048
|
timestamp_columns[action].each do |column|
|
@@ -39,7 +39,7 @@ module ActiveRecord # :nodoc:
|
|
39
39
|
|
40
40
|
next unless matched_instance
|
41
41
|
|
42
|
-
instance.
|
42
|
+
instance.instance_variable_set :@association_cache, {}
|
43
43
|
instance.send :clear_aggregation_cache if instance.respond_to?(:clear_aggregation_cache, true)
|
44
44
|
instance.instance_variable_set :@attributes, matched_instance.instance_variable_get(:@attributes)
|
45
45
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
common: &common
|
2
2
|
username: root
|
3
|
-
password:
|
3
|
+
password: root
|
4
4
|
encoding: utf8
|
5
|
+
collation: utf8_general_ci
|
5
6
|
host: localhost
|
6
7
|
database: activerecord_import_test
|
7
8
|
|
@@ -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
|
|
data/test/import_test.rb
CHANGED
@@ -169,7 +169,17 @@ describe "#import" do
|
|
169
169
|
assert_difference "Dictionary.count", +1 do
|
170
170
|
Dictionary.import dictionaries
|
171
171
|
end
|
172
|
-
assert_equal "Dictionary", Dictionary.
|
172
|
+
assert_equal "Dictionary", Dictionary.last.type
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should import arrays successfully" do
|
176
|
+
columns = [:author_name, :title]
|
177
|
+
values = [["Noah Webster", "Webster's Dictionary"]]
|
178
|
+
|
179
|
+
assert_difference "Dictionary.count", +1 do
|
180
|
+
Dictionary.import columns, values
|
181
|
+
end
|
182
|
+
assert_equal "Dictionary", Dictionary.last.type
|
173
183
|
end
|
174
184
|
end
|
175
185
|
|
@@ -252,6 +262,16 @@ describe "#import" do
|
|
252
262
|
end
|
253
263
|
end
|
254
264
|
|
265
|
+
it "should index the failed instances by their poistion in the set if `track_failures` is true" do
|
266
|
+
index_offset = valid_values.length
|
267
|
+
results = Topic.import columns, valid_values + invalid_values, validate: true, track_validation_failures: true
|
268
|
+
assert_equal invalid_values.size, results.failed_instances.size
|
269
|
+
invalid_values.each_with_index do |value_set, index|
|
270
|
+
assert_equal index + index_offset, results.failed_instances[index].first
|
271
|
+
assert_equal value_set.first, results.failed_instances[index].last.title
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
255
275
|
it "should set ids in valid models if adapter supports setting primary key of imported objects" do
|
256
276
|
if ActiveRecord::Base.supports_setting_primary_key_of_imported_objects?
|
257
277
|
Topic.import (invalid_models + valid_models), validate: true
|
@@ -395,6 +415,15 @@ describe "#import" do
|
|
395
415
|
assert_equal 3, result.num_inserts if Topic.supports_import?
|
396
416
|
end
|
397
417
|
end
|
418
|
+
|
419
|
+
it "should accept and call an optional callable to run after each batch" do
|
420
|
+
lambda_called = 0
|
421
|
+
|
422
|
+
my_proc = ->(_row_count, _batches, _batch, _duration) { lambda_called += 1 }
|
423
|
+
Topic.import Build(10, :topics), batch_size: 4, batch_progress: my_proc
|
424
|
+
|
425
|
+
assert_equal 3, lambda_called
|
426
|
+
end
|
398
427
|
end
|
399
428
|
|
400
429
|
context "with :synchronize option" do
|
@@ -642,6 +671,14 @@ describe "#import" do
|
|
642
671
|
assert_equal [val1, val2], scope.map(&column).sort
|
643
672
|
end
|
644
673
|
|
674
|
+
context "for cards and decks" do
|
675
|
+
it "works when the polymorphic name is different than base class name" do
|
676
|
+
deck = Deck.create(id: 1, name: 'test')
|
677
|
+
deck.cards.import [:id, :deck_type], [[1, 'PlayingCard']]
|
678
|
+
assert_equal deck.cards.first.deck_type, "PlayingCard"
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
645
682
|
it "works importing array of hashes" do
|
646
683
|
scope.import [{ column => val1 }, { column => val2 }]
|
647
684
|
|
@@ -900,4 +937,33 @@ describe "#import" do
|
|
900
937
|
end
|
901
938
|
end
|
902
939
|
end
|
940
|
+
describe "importing model with after_initialize callback" do
|
941
|
+
let(:columns) { %w(name size) }
|
942
|
+
let(:valid_values) { [%w("Deer", "Small"), %w("Monkey", "Medium")] }
|
943
|
+
let(:invalid_values) do
|
944
|
+
[
|
945
|
+
{ name: "giraffe", size: "Large" },
|
946
|
+
{ size: "Medium" } # name is missing
|
947
|
+
]
|
948
|
+
end
|
949
|
+
context "with validation checks turned off" do
|
950
|
+
it "should import valid data" do
|
951
|
+
Animal.import(columns, valid_values, validate: false)
|
952
|
+
assert_equal 2, Animal.count
|
953
|
+
end
|
954
|
+
it "should raise ArgumentError" do
|
955
|
+
assert_raise(ArgumentError) { Animal.import(invalid_values, validate: false) }
|
956
|
+
end
|
957
|
+
end
|
958
|
+
|
959
|
+
context "with validation checks turned on" do
|
960
|
+
it "should import valid data" do
|
961
|
+
Animal.import(columns, valid_values, validate: true)
|
962
|
+
assert_equal 2, Animal.count
|
963
|
+
end
|
964
|
+
it "should raise ArgumentError" do
|
965
|
+
assert_raise(ArgumentError) { Animal.import(invalid_values, validate: true) }
|
966
|
+
end
|
967
|
+
end
|
968
|
+
end
|
903
969
|
end
|
data/test/models/card.rb
ADDED
data/test/models/deck.rb
ADDED