activerecord-import 1.0.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yaml +78 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +91 -3
  5. data/Gemfile +6 -2
  6. data/LICENSE +21 -56
  7. data/README.markdown +61 -53
  8. data/activerecord-import.gemspec +4 -4
  9. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
  10. data/gemfiles/6.0.gemfile +2 -1
  11. data/gemfiles/6.1.gemfile +2 -1
  12. data/gemfiles/7.0.gemfile +1 -0
  13. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +4 -4
  14. data/lib/activerecord-import/adapters/abstract_adapter.rb +6 -0
  15. data/lib/activerecord-import/adapters/mysql_adapter.rb +6 -6
  16. data/lib/activerecord-import/adapters/postgresql_adapter.rb +3 -11
  17. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -15
  18. data/lib/activerecord-import/base.rb +7 -1
  19. data/lib/activerecord-import/import.rb +95 -42
  20. data/lib/activerecord-import/synchronize.rb +1 -1
  21. data/lib/activerecord-import/value_sets_parser.rb +2 -0
  22. data/lib/activerecord-import/version.rb +1 -1
  23. data/test/{travis → github}/database.yml +3 -1
  24. data/test/import_test.rb +67 -1
  25. data/test/models/animal.rb +6 -0
  26. data/test/models/card.rb +3 -0
  27. data/test/models/customer.rb +6 -0
  28. data/test/models/deck.rb +6 -0
  29. data/test/models/order.rb +6 -0
  30. data/test/models/playing_card.rb +2 -0
  31. data/test/schema/generic_schema.rb +25 -0
  32. data/test/schema/postgresql_schema.rb +14 -0
  33. data/test/support/postgresql/import_examples.rb +55 -0
  34. data/test/support/shared_examples/on_duplicate_key_update.rb +10 -0
  35. data/test/support/shared_examples/recursive_import.rb +30 -1
  36. data/test/test_helper.rb +10 -1
  37. metadata +25 -16
  38. data/.travis.yml +0 -70
  39. data/gemfiles/3.2.gemfile +0 -2
  40. data/gemfiles/4.0.gemfile +0 -2
  41. data/gemfiles/4.1.gemfile +0 -2
@@ -1,6 +1,6 @@
1
- require "active_record/connection_adapters/mysql_adapter"
2
- require "activerecord-import/adapters/mysql_adapter"
1
+ require "active_record/connection_adapters/mysql2_adapter"
2
+ require "activerecord-import/adapters/mysql2_adapter"
3
3
 
4
- class ActiveRecord::ConnectionAdapters::MysqlAdapter
5
- include ActiveRecord::Import::MysqlAdapter
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( "SHOW VARIABLES like 'max_allowed_packet'" )
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[1] : result.first[1]
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!(results, table_name, 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!(results, table_name, 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!(results, table_name, locking_column)
124
+ def increment_locking_column!(table_name, results, locking_column)
125
125
  if locking_column.present?
126
- results << "#{table_name}.`#{locking_column}`=`#{locking_column}`+1"
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
- query_cache.clear if query_cache_enabled
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( _table_name, *args ) # :nodoc:
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
- require_adapter connection_pool.spec.config[:adapter]
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
- def establish_connection_with_activerecord_import(*args)
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
- new.send(:timestamp_attributes_for_update_in_model)
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
- model = new
706
- hsh.each_pair { |k, v| model[k] = v }
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
- import_associations(models, options.dup) if options[:recursive]
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 Without Validations Or Callbacks" )
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
- model.send(single_column, result)
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
- model.send("#{column}=", result[col_index])
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
- association_primary_key = association_reflection.association_primary_key
884
- model.public_send("#{column_name}=", association.send(association_primary_key))
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 and :returning not supported for associations
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}=", model.class.base_class.name)
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 = new
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
- timestamp = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
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.send :clear_association_cache
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,3 +1,5 @@
1
+ require 'active_support/core_ext/array'
2
+
1
3
  module ActiveRecord::Import
2
4
  class ValueSetTooLargeError < StandardError
3
5
  attr_reader :size
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Import
3
- VERSION = "1.0.2".freeze
3
+ VERSION = "1.3.0".freeze
4
4
  end
5
5
  end
@@ -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.first.type
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
@@ -0,0 +1,6 @@
1
+ class Animal < ActiveRecord::Base
2
+ after_initialize :validate_name_presence, if: :new_record?
3
+ def validate_name_presence
4
+ raise ArgumentError if name.nil?
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ class Card < ActiveRecord::Base
2
+ belongs_to :deck, polymorphic: true
3
+ end
@@ -0,0 +1,6 @@
1
+ class Customer < ActiveRecord::Base
2
+ has_many :orders,
3
+ inverse_of: :customer,
4
+ primary_key: %i(account_id id),
5
+ foreign_key: %i(account_id customer_id)
6
+ end
@@ -0,0 +1,6 @@
1
+ class Deck < ActiveRecord::Base
2
+ has_many :cards
3
+ def self.polymorphic_name
4
+ "PlayingCard"
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class Order < ActiveRecord::Base
2
+ belongs_to :customer,
3
+ inverse_of: :orders,
4
+ primary_key: %i(account_id id),
5
+ foreign_key: %i(account_id customer_id)
6
+ end
@@ -0,0 +1,2 @@
1
+ class PlayingCard < ActiveRecord::Base
2
+ end