activerecord-import 1.0.1 → 1.0.7

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.
@@ -6,8 +6,8 @@ Gem::Specification.new do |gem|
6
6
  gem.email = ["zach.dennis@gmail.com"]
7
7
  gem.summary = "Bulk insert extension for ActiveRecord"
8
8
  gem.description = "A library for bulk inserting data using ActiveRecord."
9
- gem.homepage = "http://github.com/zdennis/activerecord-import"
10
- gem.license = "Ruby"
9
+ gem.homepage = "https://github.com/zdennis/activerecord-import"
10
+ gem.license = "MIT"
11
11
 
12
12
  gem.files = `git ls-files`.split($\)
13
13
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = ActiveRecord::Import::VERSION
18
18
 
19
- gem.required_ruby_version = ">= 1.9.2"
19
+ gem.required_ruby_version = ">= 2.0.0"
20
20
 
21
21
  gem.add_runtime_dependency "activerecord", ">= 3.2"
22
22
  gem.add_development_dependency "rake"
@@ -0,0 +1 @@
1
+ gem 'activerecord', '~> 6.0.0'
@@ -0,0 +1 @@
1
+ gem 'activerecord', '~> 6.1.0.alpha', github: "rails/rails"
@@ -46,7 +46,7 @@ module ActiveRecord::Import::AbstractAdapter
46
46
 
47
47
  if supports_on_duplicate_key_update? && options[:on_duplicate_key_update]
48
48
  post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update], options[:primary_key], options[:locking_column] )
49
- elsif options[:on_duplicate_key_update]
49
+ elsif logger && options[:on_duplicate_key_update]
50
50
  logger.warn "Ignoring on_duplicate_key_update because it is not supported by the database."
51
51
  end
52
52
 
@@ -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
@@ -73,7 +73,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
73
73
  if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
74
74
  sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
75
75
  end
76
- elsif options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
76
+ elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
77
77
  logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
78
78
  end
79
79
 
@@ -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
 
@@ -195,17 +195,17 @@ module ActiveRecord::Import::PostgreSQLAdapter
195
195
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
196
196
  end
197
197
 
198
- def supports_on_duplicate_key_update?(current_version = postgresql_version)
199
- current_version >= MIN_VERSION_FOR_UPSERT
198
+ def supports_on_duplicate_key_update?
199
+ database_version >= MIN_VERSION_FOR_UPSERT
200
200
  end
201
201
 
202
202
  def supports_setting_primary_key_of_imported_objects?
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
206
+ private
207
+
208
+ def database_version
209
+ defined?(postgresql_version) ? postgresql_version : super
210
210
  end
211
211
  end
@@ -9,16 +9,12 @@ module ActiveRecord::Import::SQLite3Adapter
9
9
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
10
10
  # to ensure that we only support import in supported version of SQLite.
11
11
  # Which INSERT statements with multiple value sets was introduced in 3.7.11.
12
- def supports_import?(current_version = sqlite_version)
13
- if current_version >= MIN_VERSION_FOR_IMPORT
14
- true
15
- else
16
- false
17
- end
12
+ def supports_import?
13
+ database_version >= MIN_VERSION_FOR_IMPORT
18
14
  end
19
15
 
20
- def supports_on_duplicate_key_update?(current_version = sqlite_version)
21
- current_version >= MIN_VERSION_FOR_UPSERT
16
+ def supports_on_duplicate_key_update?
17
+ database_version >= MIN_VERSION_FOR_UPSERT
22
18
  end
23
19
 
24
20
  # +sql+ can be a single string or an array. If it is an array all
@@ -96,7 +92,7 @@ module ActiveRecord::Import::SQLite3Adapter
96
92
 
97
93
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
98
94
  # in +args+.
99
- def sql_for_on_duplicate_key_update( _table_name, *args ) # :nodoc:
95
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
100
96
  arg, primary_key, locking_column = args
101
97
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
102
98
  return unless arg.is_a?( Hash )
@@ -117,9 +113,9 @@ module ActiveRecord::Import::SQLite3Adapter
117
113
 
118
114
  sql << "#{conflict_target}DO UPDATE SET "
119
115
  if columns.is_a?( Array )
120
- 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 )
121
117
  elsif columns.is_a?( Hash )
122
- 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 )
123
119
  elsif columns.is_a?( String )
124
120
  sql << columns
125
121
  else
@@ -131,22 +127,22 @@ module ActiveRecord::Import::SQLite3Adapter
131
127
  sql
132
128
  end
133
129
 
134
- 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:
135
131
  results = arr.map do |column|
136
132
  qc = quote_column_name( column )
137
133
  "#{qc}=EXCLUDED.#{qc}"
138
134
  end
139
- increment_locking_column!(results, locking_column)
135
+ increment_locking_column!(table_name, results, locking_column)
140
136
  results.join( ',' )
141
137
  end
142
138
 
143
- 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:
144
140
  results = hsh.map do |column1, column2|
145
141
  qc1 = quote_column_name( column1 )
146
142
  qc2 = quote_column_name( column2 )
147
143
  "#{qc1}=EXCLUDED.#{qc2}"
148
144
  end
149
- increment_locking_column!(results, locking_column)
145
+ increment_locking_column!(table_name, results, locking_column)
150
146
  results.join( ',' )
151
147
  end
152
148
 
@@ -170,9 +166,9 @@ module ActiveRecord::Import::SQLite3Adapter
170
166
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
171
167
  end
172
168
 
173
- def increment_locking_column!(results, locking_column)
174
- if locking_column.present?
175
- results << "\"#{locking_column}\"=EXCLUDED.\"#{locking_column}\"+1"
176
- end
169
+ private
170
+
171
+ def database_version
172
+ defined?(sqlite_version) ? sqlite_version : super
177
173
  end
178
174
  end
@@ -13,6 +13,7 @@ module ActiveRecord::Import
13
13
  when 'postgresql_makara' then 'postgresql'
14
14
  when 'makara_postgis' then 'postgresql'
15
15
  when 'postgis' then 'postgresql'
16
+ when 'cockroachdb' then 'postgresql'
16
17
  else adapter
17
18
  end
18
19
  end
@@ -26,7 +27,13 @@ module ActiveRecord::Import
26
27
 
27
28
  # Loads the import functionality for the passed in ActiveRecord connection
28
29
  def self.load_from_connection_pool(connection_pool)
29
- 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
30
37
  end
31
38
  end
32
39
 
@@ -26,6 +26,7 @@ module ActiveRecord::Import #:nodoc:
26
26
  class Validator
27
27
  def initialize(klass, options = {})
28
28
  @options = options
29
+ @validator_class = klass
29
30
  init_validations(klass)
30
31
  end
31
32
 
@@ -70,6 +71,8 @@ module ActiveRecord::Import #:nodoc:
70
71
  end
71
72
 
72
73
  def valid_model?(model)
74
+ init_validations(model.class) unless model.class == @validator_class
75
+
73
76
  validation_context = @options[:validate_with_context]
74
77
  validation_context ||= (model.new_record? ? :create : :update)
75
78
  current_context = model.send(:validation_context)
@@ -242,16 +245,17 @@ class ActiveRecord::Associations::CollectionAssociation
242
245
  alias import bulk_import unless respond_to? :import
243
246
  end
244
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
+
245
256
  class ActiveRecord::Base
246
257
  class << self
247
- def establish_connection_with_activerecord_import(*args)
248
- conn = establish_connection_without_activerecord_import(*args)
249
- ActiveRecord::Import.load_from_connection_pool connection_pool
250
- conn
251
- end
252
-
253
- alias establish_connection_without_activerecord_import establish_connection
254
- alias establish_connection establish_connection_with_activerecord_import
258
+ prepend ActiveRecord::Import::Connection
255
259
 
256
260
  # Returns true if the current database connection adapter
257
261
  # supports import functionality, otherwise returns false.
@@ -543,7 +547,7 @@ class ActiveRecord::Base
543
547
  alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
544
548
 
545
549
  def import_helper( *args )
546
- options = { validate: true, timestamps: true }
550
+ options = { validate: true, timestamps: true, track_validation_failures: false }
547
551
  options.merge!( args.pop ) if args.last.is_a? Hash
548
552
  # making sure that current model's primary key is used
549
553
  options[:primary_key] = primary_key
@@ -578,7 +582,7 @@ class ActiveRecord::Base
578
582
  if respond_to?(:timestamp_attributes_for_update, true)
579
583
  send(:timestamp_attributes_for_update).map(&:to_sym)
580
584
  else
581
- new.send(:timestamp_attributes_for_update_in_model)
585
+ allocate.send(:timestamp_attributes_for_update_in_model)
582
586
  end
583
587
  end
584
588
 
@@ -627,7 +631,7 @@ class ActiveRecord::Base
627
631
  end
628
632
  # supports empty array
629
633
  elsif args.last.is_a?( Array ) && args.last.empty?
630
- return ActiveRecord::Import::Result.new([], 0, [])
634
+ return ActiveRecord::Import::Result.new([], 0, [], [])
631
635
  # supports 2-element array and array
632
636
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
633
637
 
@@ -699,14 +703,18 @@ class ActiveRecord::Base
699
703
  # keep track of the instance and the position it is currently at. if this fails
700
704
  # validation we'll use the index to remove it from the array_of_attributes
701
705
  arr.each_with_index do |hsh, i|
702
- model = new
703
- 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
+
704
711
  next if validator.valid_model?(model)
705
712
  raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
713
+
706
714
  array_of_attributes[i] = nil
707
715
  failure = model.dup
708
716
  failure.errors.send(:initialize_dup, model.errors)
709
- failed_instances << failure
717
+ failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
710
718
  end
711
719
  array_of_attributes.compact!
712
720
  end
@@ -804,7 +812,7 @@ class ActiveRecord::Base
804
812
  result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
805
813
  batch_values,
806
814
  options,
807
- "#{model_name} Create Many Without Validations Or Callbacks" )
815
+ "#{model_name} Create Many" )
808
816
  number_inserted += result.num_inserts
809
817
  ids += result.ids
810
818
  results += result.results
@@ -838,6 +846,19 @@ class ActiveRecord::Base
838
846
  end
839
847
  end
840
848
 
849
+ deserialize_value = lambda do |column, value|
850
+ column = columns_hash[column]
851
+ return value unless column
852
+ if respond_to?(:type_caster)
853
+ type = type_for_attribute(column.name)
854
+ type.deserialize(value)
855
+ elsif column.respond_to?(:type_cast_from_database)
856
+ column.type_cast_from_database(value)
857
+ else
858
+ value
859
+ end
860
+ end
861
+
841
862
  if models.size == import_result.results.size
842
863
  columns = Array(options[:returning])
843
864
  single_column = "#{columns.first}=" if columns.size == 1
@@ -845,10 +866,12 @@ class ActiveRecord::Base
845
866
  model = models[index]
846
867
 
847
868
  if single_column
848
- model.send(single_column, result)
869
+ val = deserialize_value.call(columns.first, result)
870
+ model.send(single_column, val)
849
871
  else
850
872
  columns.each_with_index do |column, col_index|
851
- model.send("#{column}=", result[col_index])
873
+ val = deserialize_value.call(column, result[col_index])
874
+ model.send("#{column}=", val)
852
875
  end
853
876
  end
854
877
  end
@@ -869,10 +892,12 @@ class ActiveRecord::Base
869
892
 
870
893
  # Sync belongs_to association ids with foreign key field
871
894
  def load_association_ids(model)
895
+ changed_columns = model.changed
872
896
  association_reflections = model.class.reflect_on_all_associations(:belongs_to)
873
897
  association_reflections.each do |association_reflection|
874
898
  column_name = association_reflection.foreign_key
875
899
  next if association_reflection.options[:polymorphic]
900
+ next if changed_columns.include?(column_name)
876
901
  association = model.association(association_reflection.name)
877
902
  association = association.target
878
903
  next if association.blank? || model.public_send(column_name).present?
@@ -952,7 +977,7 @@ class ActiveRecord::Base
952
977
  elsif column
953
978
  if respond_to?(:type_caster) # Rails 5.0 and higher
954
979
  type = type_for_attribute(column.name)
955
- val = type.type == :boolean ? type.cast(val) : type.serialize(val)
980
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
956
981
  connection_memo.quote(val)
957
982
  elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
958
983
  connection_memo.quote(column.type_cast_from_user(val), column)
@@ -961,7 +986,7 @@ class ActiveRecord::Base
961
986
  val = serialized_attributes[column.name].dump(val)
962
987
  end
963
988
  # Fixes #443 to support binary (i.e. bytea) columns on PG
964
- val = column.type_cast(val) unless column.type.to_sym == :binary
989
+ val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
965
990
  connection_memo.quote(val, column)
966
991
  end
967
992
  else
@@ -980,7 +1005,7 @@ class ActiveRecord::Base
980
1005
  timestamp_columns[:create] = timestamp_attributes_for_create_in_model
981
1006
  timestamp_columns[:update] = timestamp_attributes_for_update_in_model
982
1007
  else
983
- instance = new
1008
+ instance = allocate
984
1009
  timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
985
1010
  timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
986
1011
  end
@@ -39,8 +39,8 @@ module ActiveRecord # :nodoc:
39
39
 
40
40
  next unless matched_instance
41
41
 
42
- instance.send :clear_aggregation_cache
43
42
  instance.send :clear_association_cache
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
 
46
46
  if instance.respond_to?(:clear_changes_information)
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Import
3
- VERSION = "1.0.1".freeze
3
+ VERSION = "1.0.7".freeze
4
4
  end
5
5
  end
data/test/import_test.rb CHANGED
@@ -252,6 +252,16 @@ describe "#import" do
252
252
  end
253
253
  end
254
254
 
255
+ it "should index the failed instances by their poistion in the set if `track_failures` is true" do
256
+ index_offset = valid_values.length
257
+ results = Topic.import columns, valid_values + invalid_values, validate: true, track_validation_failures: true
258
+ assert_equal invalid_values.size, results.failed_instances.size
259
+ invalid_values.each_with_index do |value_set, index|
260
+ assert_equal index + index_offset, results.failed_instances[index].first
261
+ assert_equal value_set.first, results.failed_instances[index].last.title
262
+ end
263
+ end
264
+
255
265
  it "should set ids in valid models if adapter supports setting primary key of imported objects" do
256
266
  if ActiveRecord::Base.supports_setting_primary_key_of_imported_objects?
257
267
  Topic.import (invalid_models + valid_models), validate: true
@@ -900,4 +910,33 @@ describe "#import" do
900
910
  end
901
911
  end
902
912
  end
913
+ describe "importing model with after_initialize callback" do
914
+ let(:columns) { %w(name size) }
915
+ let(:valid_values) { [%w("Deer", "Small"), %w("Monkey", "Medium")] }
916
+ let(:invalid_values) do
917
+ [
918
+ { name: "giraffe", size: "Large" },
919
+ { size: "Medium" } # name is missing
920
+ ]
921
+ end
922
+ context "with validation checks turned off" do
923
+ it "should import valid data" do
924
+ Animal.import(columns, valid_values, validate: false)
925
+ assert_equal 2, Animal.count
926
+ end
927
+ it "should raise ArgumentError" do
928
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: false) }
929
+ end
930
+ end
931
+
932
+ context "with validation checks turned on" do
933
+ it "should import valid data" do
934
+ Animal.import(columns, valid_values, validate: true)
935
+ assert_equal 2, Animal.count
936
+ end
937
+ it "should raise ArgumentError" do
938
+ assert_raise(ArgumentError) { Animal.import(invalid_values, validate: true) }
939
+ end
940
+ end
941
+ end
903
942
  end