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.
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