activerecord-import 1.0.1 → 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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