activerecord-import 1.0.0 → 1.0.5

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.
@@ -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(',')})"