activerecord-import 1.0.0 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,7 +7,7 @@ Gem::Specification.new do |gem|
7
7
  gem.summary = "Bulk insert extension for ActiveRecord"
8
8
  gem.description = "A library for bulk inserting data using ActiveRecord."
9
9
  gem.homepage = "http://github.com/zdennis/activerecord-import"
10
- gem.license = "Ruby"
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,8 +59,14 @@ 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
- false
69
+ true
64
70
  end
65
71
  end
66
72
  end
@@ -1,6 +1,5 @@
1
1
  module ActiveRecord::Import::MysqlAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
- include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
3
 
5
4
  NO_MAX_PACKET = 0
6
5
  QUERY_OVERHEAD = 8 # This was shown to be true for MySQL, but it's not clear where the overhead is from.
@@ -102,7 +101,7 @@ module ActiveRecord::Import::MysqlAdapter
102
101
  qc = quote_column_name( column )
103
102
  "#{table_name}.#{qc}=VALUES(#{qc})"
104
103
  end
105
- increment_locking_column!(results, table_name, locking_column)
104
+ increment_locking_column!(table_name, results, locking_column)
106
105
  results.join( ',' )
107
106
  end
108
107
 
@@ -112,7 +111,7 @@ module ActiveRecord::Import::MysqlAdapter
112
111
  qc2 = quote_column_name( column2 )
113
112
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
114
113
  end
115
- increment_locking_column!(results, table_name, locking_column)
114
+ increment_locking_column!(table_name, results, locking_column)
116
115
  results.join( ',')
117
116
  end
118
117
 
@@ -121,9 +120,9 @@ module ActiveRecord::Import::MysqlAdapter
121
120
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
122
121
  end
123
122
 
124
- def increment_locking_column!(results, table_name, locking_column)
123
+ def increment_locking_column!(table_name, results, locking_column)
125
124
  if locking_column.present?
126
- results << "#{table_name}.`#{locking_column}`=`#{locking_column}`+1"
125
+ results << "`#{locking_column}`=#{table_name}.`#{locking_column}`+1"
127
126
  end
128
127
  end
129
128
  end
@@ -1,6 +1,5 @@
1
1
  module ActiveRecord::Import::PostgreSQLAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
- include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
3
 
5
4
  MIN_VERSION_FOR_UPSERT = 90_500
6
5
 
@@ -19,7 +18,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
19
18
  sql2insert = base_sql + values.join( ',' ) + post_sql
20
19
 
21
20
  columns = returning_columns(options)
22
- if columns.blank? || options[:no_returning]
21
+ if columns.blank? || (options[:no_returning] && !options[:recursive])
23
22
  insert( sql2insert, *args )
24
23
  else
25
24
  returned_values = if columns.size > 1
@@ -73,14 +72,14 @@ module ActiveRecord::Import::PostgreSQLAdapter
73
72
  if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update] && !options[:recursive]
74
73
  sql << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
75
74
  end
76
- elsif options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
75
+ elsif logger && options[:on_duplicate_key_ignore] && !options[:on_duplicate_key_update]
77
76
  logger.warn "Ignoring on_duplicate_key_ignore because it is not supported by the database."
78
77
  end
79
78
 
80
79
  sql += super(table_name, options)
81
80
 
82
81
  columns = returning_columns(options)
83
- unless columns.blank? || options[:no_returning]
82
+ unless columns.blank? || (options[:no_returning] && !options[:recursive])
84
83
  sql << " RETURNING \"#{columns.join('", "')}\""
85
84
  end
86
85
 
@@ -158,7 +157,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
158
157
  qc = quote_column_name( column )
159
158
  "#{qc}=EXCLUDED.#{qc}"
160
159
  end
161
- increment_locking_column!(results, locking_column)
160
+ increment_locking_column!(table_name, results, locking_column)
162
161
  results.join( ',' )
163
162
  end
164
163
 
@@ -168,7 +167,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
168
167
  qc2 = quote_column_name( column2 )
169
168
  "#{qc1}=EXCLUDED.#{qc2}"
170
169
  end
171
- increment_locking_column!(results, locking_column)
170
+ increment_locking_column!(table_name, results, locking_column)
172
171
  results.join( ',' )
173
172
  end
174
173
 
@@ -195,17 +194,17 @@ module ActiveRecord::Import::PostgreSQLAdapter
195
194
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
196
195
  end
197
196
 
198
- def supports_on_duplicate_key_update?(current_version = postgresql_version)
199
- current_version >= MIN_VERSION_FOR_UPSERT
197
+ def supports_on_duplicate_key_update?
198
+ database_version >= MIN_VERSION_FOR_UPSERT
200
199
  end
201
200
 
202
201
  def supports_setting_primary_key_of_imported_objects?
203
202
  true
204
203
  end
205
204
 
206
- def increment_locking_column!(results, locking_column)
207
- if locking_column.present?
208
- results << "\"#{locking_column}\"=EXCLUDED.\"#{locking_column}\"+1"
209
- end
205
+ private
206
+
207
+ def database_version
208
+ defined?(postgresql_version) ? postgresql_version : super
210
209
  end
211
210
  end
@@ -1,6 +1,5 @@
1
1
  module ActiveRecord::Import::SQLite3Adapter
2
2
  include ActiveRecord::Import::ImportSupport
3
- include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
3
 
5
4
  MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
6
5
  MIN_VERSION_FOR_UPSERT = "3.24.0".freeze
@@ -9,16 +8,12 @@ module ActiveRecord::Import::SQLite3Adapter
9
8
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
10
9
  # to ensure that we only support import in supported version of SQLite.
11
10
  # 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
11
+ def supports_import?
12
+ database_version >= MIN_VERSION_FOR_IMPORT
18
13
  end
19
14
 
20
- def supports_on_duplicate_key_update?(current_version = sqlite_version)
21
- current_version >= MIN_VERSION_FOR_UPSERT
15
+ def supports_on_duplicate_key_update?
16
+ database_version >= MIN_VERSION_FOR_UPSERT
22
17
  end
23
18
 
24
19
  # +sql+ can be a single string or an array. If it is an array all
@@ -96,7 +91,7 @@ module ActiveRecord::Import::SQLite3Adapter
96
91
 
97
92
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
98
93
  # in +args+.
99
- def sql_for_on_duplicate_key_update( _table_name, *args ) # :nodoc:
94
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
100
95
  arg, primary_key, locking_column = args
101
96
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
102
97
  return unless arg.is_a?( Hash )
@@ -117,9 +112,9 @@ module ActiveRecord::Import::SQLite3Adapter
117
112
 
118
113
  sql << "#{conflict_target}DO UPDATE SET "
119
114
  if columns.is_a?( Array )
120
- sql << sql_for_on_duplicate_key_update_as_array( locking_column, columns )
115
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
121
116
  elsif columns.is_a?( Hash )
122
- sql << sql_for_on_duplicate_key_update_as_hash( locking_column, columns )
117
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
123
118
  elsif columns.is_a?( String )
124
119
  sql << columns
125
120
  else
@@ -131,22 +126,22 @@ module ActiveRecord::Import::SQLite3Adapter
131
126
  sql
132
127
  end
133
128
 
134
- def sql_for_on_duplicate_key_update_as_array( locking_column, arr ) # :nodoc:
129
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
135
130
  results = arr.map do |column|
136
131
  qc = quote_column_name( column )
137
132
  "#{qc}=EXCLUDED.#{qc}"
138
133
  end
139
- increment_locking_column!(results, locking_column)
134
+ increment_locking_column!(table_name, results, locking_column)
140
135
  results.join( ',' )
141
136
  end
142
137
 
143
- def sql_for_on_duplicate_key_update_as_hash( locking_column, hsh ) # :nodoc:
138
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
144
139
  results = hsh.map do |column1, column2|
145
140
  qc1 = quote_column_name( column1 )
146
141
  qc2 = quote_column_name( column2 )
147
142
  "#{qc1}=EXCLUDED.#{qc2}"
148
143
  end
149
- increment_locking_column!(results, locking_column)
144
+ increment_locking_column!(table_name, results, locking_column)
150
145
  results.join( ',' )
151
146
  end
152
147
 
@@ -170,9 +165,9 @@ module ActiveRecord::Import::SQLite3Adapter
170
165
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
171
166
  end
172
167
 
173
- def increment_locking_column!(results, locking_column)
174
- if locking_column.present?
175
- results << "\"#{locking_column}\"=EXCLUDED.\"#{locking_column}\"+1"
176
- end
168
+ private
169
+
170
+ def database_version
171
+ defined?(sqlite_version) ? sqlite_version : super
177
172
  end
178
173
  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
 
@@ -11,12 +11,6 @@ module ActiveRecord::Import #:nodoc:
11
11
  end
12
12
  end
13
13
 
14
- module OnDuplicateKeyUpdateSupport #:nodoc:
15
- def supports_on_duplicate_key_update? #:nodoc:
16
- true
17
- end
18
- end
19
-
20
14
  class MissingColumnError < StandardError
21
15
  def initialize(name, index)
22
16
  super "Missing column for value <#{name}> at index #{index}"
@@ -26,6 +20,7 @@ module ActiveRecord::Import #:nodoc:
26
20
  class Validator
27
21
  def initialize(klass, options = {})
28
22
  @options = options
23
+ @validator_class = klass
29
24
  init_validations(klass)
30
25
  end
31
26
 
@@ -70,6 +65,8 @@ module ActiveRecord::Import #:nodoc:
70
65
  end
71
66
 
72
67
  def valid_model?(model)
68
+ init_validations(model.class) unless model.class == @validator_class
69
+
73
70
  validation_context = @options[:validate_with_context]
74
71
  validation_context ||= (model.new_record? ? :create : :update)
75
72
  current_context = model.send(:validation_context)
@@ -242,16 +239,17 @@ class ActiveRecord::Associations::CollectionAssociation
242
239
  alias import bulk_import unless respond_to? :import
243
240
  end
244
241
 
242
+ module ActiveRecord::Import::Connection
243
+ def establish_connection(args = nil)
244
+ conn = super(args)
245
+ ActiveRecord::Import.load_from_connection_pool connection_pool
246
+ conn
247
+ end
248
+ end
249
+
245
250
  class ActiveRecord::Base
246
251
  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
252
+ prepend ActiveRecord::Import::Connection
255
253
 
256
254
  # Returns true if the current database connection adapter
257
255
  # supports import functionality, otherwise returns false.
@@ -528,7 +526,7 @@ class ActiveRecord::Base
528
526
  import_helper(*args)
529
527
  end
530
528
  end
531
- alias import bulk_import unless respond_to? :import
529
+ alias import bulk_import unless ActiveRecord::Base.respond_to? :import
532
530
 
533
531
  # Imports a collection of values if all values are valid. Import fails at the
534
532
  # first encountered validation error and raises ActiveRecord::RecordInvalid
@@ -540,7 +538,7 @@ class ActiveRecord::Base
540
538
 
541
539
  bulk_import(*args, options)
542
540
  end
543
- alias import! bulk_import! unless respond_to? :import!
541
+ alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
544
542
 
545
543
  def import_helper( *args )
546
544
  options = { validate: true, timestamps: true }
@@ -574,15 +572,6 @@ class ActiveRecord::Base
574
572
  end
575
573
  end
576
574
 
577
- default_values = column_defaults
578
- stored_attrs = respond_to?(:stored_attributes) ? stored_attributes : {}
579
- serialized_attrs = if defined?(ActiveRecord::Type::Serialized)
580
- attrs = column_names.select { |c| type_for_attribute(c.to_s).class == ActiveRecord::Type::Serialized }
581
- Hash[attrs.map { |a| [a, nil] }]
582
- else
583
- serialized_attributes
584
- end
585
-
586
575
  update_attrs = if record_timestamps && options[:timestamps]
587
576
  if respond_to?(:timestamp_attributes_for_update, true)
588
577
  send(:timestamp_attributes_for_update).map(&:to_sym)
@@ -608,12 +597,8 @@ class ActiveRecord::Base
608
597
  update_attrs && update_attrs.include?(name.to_sym) &&
609
598
  !model.send("#{name}_changed?")
610
599
  nil
611
- elsif stored_attrs.key?(name.to_sym) ||
612
- serialized_attrs.key?(name.to_s) ||
613
- default_values[name.to_s]
614
- model.read_attribute(name.to_s)
615
600
  else
616
- model.read_attribute_before_type_cast(name.to_s)
601
+ model.read_attribute(name.to_s)
617
602
  end
618
603
  end
619
604
  end
@@ -640,7 +625,7 @@ class ActiveRecord::Base
640
625
  end
641
626
  # supports empty array
642
627
  elsif args.last.is_a?( Array ) && args.last.empty?
643
- return ActiveRecord::Import::Result.new([], 0, [])
628
+ return ActiveRecord::Import::Result.new([], 0, [], [])
644
629
  # supports 2-element array and array
645
630
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
646
631
 
@@ -851,6 +836,19 @@ class ActiveRecord::Base
851
836
  end
852
837
  end
853
838
 
839
+ deserialize_value = lambda do |column, value|
840
+ column = columns_hash[column]
841
+ return value unless column
842
+ if respond_to?(:type_caster)
843
+ type = type_for_attribute(column.name)
844
+ type.deserialize(value)
845
+ elsif column.respond_to?(:type_cast_from_database)
846
+ column.type_cast_from_database(value)
847
+ else
848
+ value
849
+ end
850
+ end
851
+
854
852
  if models.size == import_result.results.size
855
853
  columns = Array(options[:returning])
856
854
  single_column = "#{columns.first}=" if columns.size == 1
@@ -858,10 +856,12 @@ class ActiveRecord::Base
858
856
  model = models[index]
859
857
 
860
858
  if single_column
861
- model.send(single_column, result)
859
+ val = deserialize_value.call(columns.first, result)
860
+ model.send(single_column, val)
862
861
  else
863
862
  columns.each_with_index do |column, col_index|
864
- model.send("#{column}=", result[col_index])
863
+ val = deserialize_value.call(column, result[col_index])
864
+ model.send("#{column}=", val)
865
865
  end
866
866
  end
867
867
  end
@@ -882,10 +882,12 @@ class ActiveRecord::Base
882
882
 
883
883
  # Sync belongs_to association ids with foreign key field
884
884
  def load_association_ids(model)
885
+ changed_columns = model.changed
885
886
  association_reflections = model.class.reflect_on_all_associations(:belongs_to)
886
887
  association_reflections.each do |association_reflection|
887
888
  column_name = association_reflection.foreign_key
888
889
  next if association_reflection.options[:polymorphic]
890
+ next if changed_columns.include?(column_name)
889
891
  association = model.association(association_reflection.name)
890
892
  association = association.target
891
893
  next if association.blank? || model.public_send(column_name).present?
@@ -904,8 +906,9 @@ class ActiveRecord::Base
904
906
  associated_objects_by_class = {}
905
907
  models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
906
908
 
907
- # :on_duplicate_key_update not supported for associations
909
+ # :on_duplicate_key_update and :returning not supported for associations
908
910
  options.delete(:on_duplicate_key_update)
911
+ options.delete(:returning)
909
912
 
910
913
  associated_objects_by_class.each_value do |associations|
911
914
  associations.each_value do |associated_records|
@@ -964,7 +967,7 @@ class ActiveRecord::Base
964
967
  elsif column
965
968
  if respond_to?(:type_caster) # Rails 5.0 and higher
966
969
  type = type_for_attribute(column.name)
967
- val = type.type == :boolean ? type.cast(val) : type.serialize(val)
970
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
968
971
  connection_memo.quote(val)
969
972
  elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
970
973
  connection_memo.quote(column.type_cast_from_user(val), column)
@@ -973,9 +976,11 @@ class ActiveRecord::Base
973
976
  val = serialized_attributes[column.name].dump(val)
974
977
  end
975
978
  # Fixes #443 to support binary (i.e. bytea) columns on PG
976
- val = column.type_cast(val) unless column.type.to_sym == :binary
979
+ val = column.type_cast(val) unless column.type && column.type.to_sym == :binary
977
980
  connection_memo.quote(val, column)
978
981
  end
982
+ else
983
+ raise ArgumentError, "Number of values (#{arr.length}) exceeds number of columns (#{columns.length})"
979
984
  end
980
985
  end
981
986
  "(#{my_values.join(',')})"