activerecord-import 1.4.1 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yaml +53 -13
  3. data/.gitignore +4 -0
  4. data/.rubocop.yml +7 -4
  5. data/.rubocop_todo.yml +10 -16
  6. data/CHANGELOG.md +48 -1
  7. data/Dockerfile +23 -0
  8. data/Gemfile +15 -7
  9. data/README.markdown +44 -5
  10. data/Rakefile +1 -0
  11. data/activerecord-import.gemspec +4 -0
  12. data/benchmarks/benchmark.rb +3 -3
  13. data/benchmarks/lib/base.rb +2 -2
  14. data/benchmarks/lib/cli_parser.rb +2 -2
  15. data/docker-compose.yml +34 -0
  16. data/gemfiles/7.1.gemfile +3 -0
  17. data/gemfiles/7.2.gemfile +3 -0
  18. data/lib/activerecord-import/active_record/adapters/trilogy_adapter.rb +8 -0
  19. data/lib/activerecord-import/adapters/abstract_adapter.rb +6 -5
  20. data/lib/activerecord-import/adapters/mysql_adapter.rb +24 -18
  21. data/lib/activerecord-import/adapters/postgresql_adapter.rb +26 -18
  22. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +29 -23
  23. data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
  24. data/lib/activerecord-import/import.rb +63 -28
  25. data/lib/activerecord-import/value_sets_parser.rb +1 -0
  26. data/lib/activerecord-import/version.rb +1 -1
  27. data/lib/activerecord-import.rb +0 -1
  28. data/test/adapters/trilogy.rb +9 -0
  29. data/test/database.yml.sample +7 -0
  30. data/test/github/database.yml +4 -0
  31. data/test/jdbcmysql/import_test.rb +3 -3
  32. data/test/jdbcpostgresql/import_test.rb +2 -2
  33. data/test/jdbcsqlite3/import_test.rb +2 -2
  34. data/test/makara_postgis/import_test.rb +2 -2
  35. data/test/models/author.rb +7 -0
  36. data/test/models/bike_maker.rb +1 -0
  37. data/test/models/book.rb +5 -2
  38. data/test/models/composite_book.rb +19 -0
  39. data/test/models/composite_chapter.rb +9 -0
  40. data/test/models/customer.rb +14 -4
  41. data/test/models/order.rb +13 -4
  42. data/test/models/tag.rb +6 -1
  43. data/test/models/tag_alias.rb +7 -1
  44. data/test/models/topic.rb +5 -0
  45. data/test/models/widget.rb +10 -3
  46. data/test/mysql2/import_test.rb +3 -3
  47. data/test/mysql2_makara/import_test.rb +3 -3
  48. data/test/mysqlspatial2/import_test.rb +3 -3
  49. data/test/postgis/import_test.rb +2 -2
  50. data/test/postgresql/import_test.rb +2 -2
  51. data/test/schema/generic_schema.rb +4 -1
  52. data/test/schema/jdbcpostgresql_schema.rb +1 -1
  53. data/test/schema/postgis_schema.rb +1 -1
  54. data/test/schema/postgresql_schema.rb +35 -4
  55. data/test/sqlite3/import_test.rb +2 -2
  56. data/test/support/postgresql/import_examples.rb +12 -0
  57. data/test/support/shared_examples/on_duplicate_key_update.rb +67 -10
  58. data/test/support/shared_examples/recursive_import.rb +67 -1
  59. data/test/test_helper.rb +6 -4
  60. data/test/trilogy/import_test.rb +7 -0
  61. data/test/value_sets_bytes_parser_test.rb +1 -1
  62. data/test/value_sets_records_parser_test.rb +1 -1
  63. metadata +24 -7
@@ -13,13 +13,14 @@ module ActiveRecord::Import::MysqlAdapter
13
13
  # the number of inserts default
14
14
  number_of_inserts = 0
15
15
 
16
- base_sql, post_sql = if sql.is_a?( String )
17
- [sql, '']
18
- elsif sql.is_a?( Array )
19
- [sql.shift, sql.join( ' ' )]
16
+ base_sql, post_sql = case sql
17
+ when String
18
+ [sql, '']
19
+ when Array
20
+ [sql.shift, sql.join( ' ' )]
20
21
  end
21
22
 
22
- sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
23
+ sql_size = QUERY_OVERHEAD + base_sql.bytesize + post_sql.bytesize
23
24
 
24
25
  # the number of bytes the requested insert statement values will take up
25
26
  values_in_bytes = values.sum(&:bytesize)
@@ -33,7 +34,7 @@ module ActiveRecord::Import::MysqlAdapter
33
34
  max = max_allowed_packet
34
35
 
35
36
  # if we can insert it all as one statement
36
- if NO_MAX_PACKET == max || total_bytes <= max || options[:force_single_insert]
37
+ if max == NO_MAX_PACKET || total_bytes <= max || options[:force_single_insert]
37
38
  number_of_inserts += 1
38
39
  sql2insert = base_sql + values.join( ',' ) + post_sql
39
40
  insert( sql2insert, *args )
@@ -85,13 +86,13 @@ module ActiveRecord::Import::MysqlAdapter
85
86
  # in +args+.
86
87
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
87
88
  sql = ' ON DUPLICATE KEY UPDATE '.dup
88
- arg = args.first
89
- locking_column = args.last
90
- if arg.is_a?( Array )
91
- sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arg )
92
- elsif arg.is_a?( Hash )
93
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, arg )
94
- elsif arg.is_a?( String )
89
+ arg, model, _primary_key, locking_column = args
90
+ case arg
91
+ when Array
92
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arg )
93
+ when Hash
94
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, arg )
95
+ when String
95
96
  sql << arg
96
97
  else
97
98
  raise ArgumentError, "Expected Array or Hash"
@@ -99,19 +100,24 @@ module ActiveRecord::Import::MysqlAdapter
99
100
  sql
100
101
  end
101
102
 
102
- def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
103
+ def sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arr ) # :nodoc:
103
104
  results = arr.map do |column|
104
- qc = quote_column_name( column )
105
+ original_column_name = model.attribute_alias?( column ) ? model.attribute_alias( column ) : column
106
+ qc = quote_column_name( original_column_name )
105
107
  "#{table_name}.#{qc}=VALUES(#{qc})"
106
108
  end
107
109
  increment_locking_column!(table_name, results, locking_column)
108
110
  results.join( ',' )
109
111
  end
110
112
 
111
- def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
113
+ def sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, hsh ) # :nodoc:
112
114
  results = hsh.map do |column1, column2|
113
- qc1 = quote_column_name( column1 )
114
- qc2 = quote_column_name( column2 )
115
+ original_column1_name = model.attribute_alias?( column1 ) ? model.attribute_alias( column1 ) : column1
116
+ qc1 = quote_column_name( original_column1_name )
117
+
118
+ original_column2_name = model.attribute_alias?( column2 ) ? model.attribute_alias( column2 ) : column2
119
+ qc2 = quote_column_name( original_column2_name )
120
+
115
121
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
116
122
  end
117
123
  increment_locking_column!(table_name, results, locking_column)
@@ -12,10 +12,11 @@ module ActiveRecord::Import::PostgreSQLAdapter
12
12
  ids = []
13
13
  results = []
14
14
 
15
- base_sql, post_sql = if sql.is_a?( String )
16
- [sql, '']
17
- elsif sql.is_a?( Array )
18
- [sql.shift, sql.join( ' ' )]
15
+ base_sql, post_sql = case sql
16
+ when String
17
+ [sql, '']
18
+ when Array
19
+ [sql.shift, sql.join( ' ' )]
19
20
  end
20
21
 
21
22
  sql2insert = base_sql + values.join( ',' ) + post_sql
@@ -110,13 +111,14 @@ module ActiveRecord::Import::PostgreSQLAdapter
110
111
  # Add a column to be updated on duplicate key update
111
112
  def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
112
113
  arg = options[:on_duplicate_key_update]
113
- if arg.is_a?( Hash )
114
+ case arg
115
+ when Hash
114
116
  columns = arg.fetch( :columns ) { arg[:columns] = [] }
115
117
  case columns
116
118
  when Array then columns << column.to_sym unless columns.include?( column.to_sym )
117
119
  when Hash then columns[column.to_sym] = column.to_sym
118
120
  end
119
- elsif arg.is_a?( Array )
121
+ when Array
120
122
  arg << column.to_sym unless arg.include?( column.to_sym )
121
123
  end
122
124
  end
@@ -132,7 +134,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
132
134
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
133
135
  # in +args+.
134
136
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
135
- arg, primary_key, locking_column = args
137
+ arg, model, primary_key, locking_column = args
136
138
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
137
139
  return unless arg.is_a?( Hash )
138
140
 
@@ -151,11 +153,12 @@ module ActiveRecord::Import::PostgreSQLAdapter
151
153
  end
152
154
 
153
155
  sql << "#{conflict_target}DO UPDATE SET "
154
- if columns.is_a?( Array )
155
- sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
156
- elsif columns.is_a?( Hash )
157
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
158
- elsif columns.is_a?( String )
156
+ case columns
157
+ when Array
158
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, columns )
159
+ when Hash
160
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, columns )
161
+ when String
159
162
  sql << columns
160
163
  else
161
164
  raise ArgumentError, 'Expected :columns to be an Array or Hash'
@@ -166,19 +169,24 @@ module ActiveRecord::Import::PostgreSQLAdapter
166
169
  sql
167
170
  end
168
171
 
169
- def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
172
+ def sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arr ) # :nodoc:
170
173
  results = arr.map do |column|
171
- qc = quote_column_name( column )
174
+ original_column_name = model.attribute_alias?( column ) ? model.attribute_alias( column ) : column
175
+ qc = quote_column_name( original_column_name )
172
176
  "#{qc}=EXCLUDED.#{qc}"
173
177
  end
174
178
  increment_locking_column!(table_name, results, locking_column)
175
179
  results.join( ',' )
176
180
  end
177
181
 
178
- def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
182
+ def sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, hsh ) # :nodoc:
179
183
  results = hsh.map do |column1, column2|
180
- qc1 = quote_column_name( column1 )
181
- qc2 = quote_column_name( column2 )
184
+ original_column1_name = model.attribute_alias?( column1 ) ? model.attribute_alias( column1 ) : column1
185
+ qc1 = quote_column_name( original_column1_name )
186
+
187
+ original_column2_name = model.attribute_alias?( column2 ) ? model.attribute_alias( column2 ) : column2
188
+ qc2 = quote_column_name( original_column2_name )
189
+
182
190
  "#{qc1}=EXCLUDED.#{qc2}"
183
191
  end
184
192
  increment_locking_column!(table_name, results, locking_column)
@@ -192,7 +200,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
192
200
  if constraint_name.present?
193
201
  "ON CONSTRAINT #{constraint_name} "
194
202
  elsif conflict_target.present?
195
- sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
203
+ sql = "(#{Array( conflict_target ).reject( &:blank? ).join( ', ' )}) "
196
204
  sql += "WHERE #{index_predicate} " if index_predicate
197
205
  sql
198
206
  end
@@ -24,10 +24,11 @@ module ActiveRecord::Import::SQLite3Adapter
24
24
  def insert_many( sql, values, _options = {}, *args ) # :nodoc:
25
25
  number_of_inserts = 0
26
26
 
27
- base_sql, post_sql = if sql.is_a?( String )
28
- [sql, '']
29
- elsif sql.is_a?( Array )
30
- [sql.shift, sql.join( ' ' )]
27
+ base_sql, post_sql = case sql
28
+ when String
29
+ [sql, '']
30
+ when Array
31
+ [sql.shift, sql.join( ' ' )]
31
32
  end
32
33
 
33
34
  value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
@@ -56,11 +57,9 @@ module ActiveRecord::Import::SQLite3Adapter
56
57
  def post_sql_statements( table_name, options ) # :nodoc:
57
58
  sql = []
58
59
 
59
- if supports_on_duplicate_key_update?
60
- # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
61
- if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update]
62
- sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
63
- end
60
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
61
+ if supports_on_duplicate_key_update? && ((options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update])
62
+ sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
64
63
  end
65
64
 
66
65
  sql + super
@@ -73,13 +72,14 @@ module ActiveRecord::Import::SQLite3Adapter
73
72
  # Add a column to be updated on duplicate key update
74
73
  def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
75
74
  arg = options[:on_duplicate_key_update]
76
- if arg.is_a?( Hash )
75
+ case arg
76
+ when Hash
77
77
  columns = arg.fetch( :columns ) { arg[:columns] = [] }
78
78
  case columns
79
79
  when Array then columns << column.to_sym unless columns.include?( column.to_sym )
80
80
  when Hash then columns[column.to_sym] = column.to_sym
81
81
  end
82
- elsif arg.is_a?( Array )
82
+ when Array
83
83
  arg << column.to_sym unless arg.include?( column.to_sym )
84
84
  end
85
85
  end
@@ -95,7 +95,7 @@ module ActiveRecord::Import::SQLite3Adapter
95
95
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
96
96
  # in +args+.
97
97
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
98
- arg, primary_key, locking_column = args
98
+ arg, model, primary_key, locking_column = args
99
99
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
100
100
  return unless arg.is_a?( Hash )
101
101
 
@@ -114,11 +114,12 @@ module ActiveRecord::Import::SQLite3Adapter
114
114
  end
115
115
 
116
116
  sql << "#{conflict_target}DO UPDATE SET "
117
- if columns.is_a?( Array )
118
- sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
119
- elsif columns.is_a?( Hash )
120
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
121
- elsif columns.is_a?( String )
117
+ case columns
118
+ when Array
119
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, columns )
120
+ when Hash
121
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, columns )
122
+ when String
122
123
  sql << columns
123
124
  else
124
125
  raise ArgumentError, 'Expected :columns to be an Array or Hash'
@@ -129,19 +130,24 @@ module ActiveRecord::Import::SQLite3Adapter
129
130
  sql
130
131
  end
131
132
 
132
- def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
133
+ def sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arr ) # :nodoc:
133
134
  results = arr.map do |column|
134
- qc = quote_column_name( column )
135
+ original_column_name = model.attribute_alias?( column ) ? model.attribute_alias( column ) : column
136
+ qc = quote_column_name( original_column_name )
135
137
  "#{qc}=EXCLUDED.#{qc}"
136
138
  end
137
139
  increment_locking_column!(table_name, results, locking_column)
138
140
  results.join( ',' )
139
141
  end
140
142
 
141
- def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
143
+ def sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, hsh ) # :nodoc:
142
144
  results = hsh.map do |column1, column2|
143
- qc1 = quote_column_name( column1 )
144
- qc2 = quote_column_name( column2 )
145
+ original_column1_name = model.attribute_alias?( column1 ) ? model.attribute_alias( column1 ) : column1
146
+ qc1 = quote_column_name( original_column1_name )
147
+
148
+ original_column2_name = model.attribute_alias?( column2 ) ? model.attribute_alias( column2 ) : column2
149
+ qc2 = quote_column_name( original_column2_name )
150
+
145
151
  "#{qc1}=EXCLUDED.#{qc2}"
146
152
  end
147
153
  increment_locking_column!(table_name, results, locking_column)
@@ -152,7 +158,7 @@ module ActiveRecord::Import::SQLite3Adapter
152
158
  conflict_target = args[:conflict_target]
153
159
  index_predicate = args[:index_predicate]
154
160
  if conflict_target.present?
155
- sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
161
+ sql = "(#{Array( conflict_target ).reject( &:blank? ).join( ', ' )}) "
156
162
  sql += "WHERE #{index_predicate} " if index_predicate
157
163
  sql
158
164
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "activerecord-import/adapters/mysql_adapter"
4
+
5
+ module ActiveRecord::Import::TrilogyAdapter
6
+ include ActiveRecord::Import::MysqlAdapter
7
+ end
@@ -4,17 +4,17 @@ require "ostruct"
4
4
 
5
5
  module ActiveRecord::Import::ConnectionAdapters; end
6
6
 
7
- module ActiveRecord::Import #:nodoc:
7
+ module ActiveRecord::Import # :nodoc:
8
8
  Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
9
9
 
10
- module ImportSupport #:nodoc:
11
- def supports_import? #:nodoc:
10
+ module ImportSupport # :nodoc:
11
+ def supports_import? # :nodoc:
12
12
  true
13
13
  end
14
14
  end
15
15
 
16
- module OnDuplicateKeyUpdateSupport #:nodoc:
17
- def supports_on_duplicate_key_update? #:nodoc:
16
+ module OnDuplicateKeyUpdateSupport # :nodoc:
17
+ def supports_on_duplicate_key_update? # :nodoc:
18
18
  true
19
19
  end
20
20
  end
@@ -62,6 +62,7 @@ module ActiveRecord::Import #:nodoc:
62
62
  if @validate_callbacks.respond_to?(:chain, true)
63
63
  @validate_callbacks.send(:chain).tap do |chain|
64
64
  callback.instance_variable_set(:@filter, filter)
65
+ callback.instance_variable_set(:@compiled, nil)
65
66
  chain[i] = callback
66
67
  end
67
68
  else
@@ -73,7 +74,7 @@ module ActiveRecord::Import #:nodoc:
73
74
  end
74
75
 
75
76
  def valid_model?(model)
76
- init_validations(model.class) unless model.class == @validator_class
77
+ init_validations(model.class) unless model.instance_of?(@validator_class)
77
78
 
78
79
  validation_context = @options[:validate_with_context]
79
80
  validation_context ||= (model.new_record? ? :create : :update)
@@ -85,11 +86,15 @@ module ActiveRecord::Import #:nodoc:
85
86
 
86
87
  model.run_callbacks(:validation) do
87
88
  if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
88
- runner = @validate_callbacks.compile
89
+ runner = if @validate_callbacks.method(:compile).arity == 0
90
+ @validate_callbacks.compile
91
+ else # ActiveRecord >= 7.1
92
+ @validate_callbacks.compile(nil)
93
+ end
89
94
  env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
90
95
  if runner.respond_to?(:call) # ActiveRecord < 5.1
91
96
  runner.call(env)
92
- else # ActiveRecord 5.1
97
+ else # ActiveRecord >= 5.1
93
98
  # Note that this is a gross simplification of ActiveSupport::Callbacks#run_callbacks.
94
99
  # It's technically possible for there to exist an "around" callback in the
95
100
  # :validate chain, but this would be an aberration, since Rails doesn't define
@@ -102,7 +107,8 @@ module ActiveRecord::Import #:nodoc:
102
107
  # no real-world use case for it.
103
108
  raise "The :validate callback chain contains an 'around' callback, which is unsupported" unless runner.final?
104
109
  runner.invoke_before(env)
105
- runner.invoke_after(env)
110
+ # Ensure a truthy value is returned. ActiveRecord < 7.2 always returned an array.
111
+ runner.invoke_after(env) || []
106
112
  end
107
113
  elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
108
114
  model.instance_eval @validate_callbacks.compile
@@ -165,7 +171,7 @@ class ActiveRecord::Associations::CollectionAssociation
165
171
  m.public_send "#{reflection.type}=", owner.class.name if reflection.type
166
172
  end
167
173
 
168
- return model_klass.bulk_import column_names, models, options
174
+ model_klass.bulk_import column_names, models, options
169
175
 
170
176
  # supports array of hash objects
171
177
  elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
@@ -204,11 +210,11 @@ class ActiveRecord::Associations::CollectionAssociation
204
210
  end
205
211
  end
206
212
 
207
- return model_klass.bulk_import column_names, array_of_attributes, options
213
+ model_klass.bulk_import column_names, array_of_attributes, options
208
214
 
209
215
  # supports empty array
210
216
  elsif args.last.is_a?( Array ) && args.last.empty?
211
- return ActiveRecord::Import::Result.new([], 0, [])
217
+ ActiveRecord::Import::Result.new([], 0, [])
212
218
 
213
219
  # supports 2-element array and array
214
220
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
@@ -239,7 +245,7 @@ class ActiveRecord::Associations::CollectionAssociation
239
245
  end
240
246
  end
241
247
 
242
- return model_klass.bulk_import column_names, array_of_attributes, options
248
+ model_klass.bulk_import column_names, array_of_attributes, options
243
249
  else
244
250
  raise ArgumentError, "Invalid arguments!"
245
251
  end
@@ -553,7 +559,7 @@ class ActiveRecord::Base
553
559
  options.merge!( args.pop ) if args.last.is_a? Hash
554
560
  # making sure that current model's primary key is used
555
561
  options[:primary_key] = primary_key
556
- options[:locking_column] = locking_column if attribute_names.include?(locking_column)
562
+ options[:locking_column] = locking_column if locking_enabled?
557
563
 
558
564
  is_validating = options[:validate_with_context].present? ? true : options[:validate]
559
565
  validator = ActiveRecord::Import::Validator.new(self, options)
@@ -574,7 +580,7 @@ class ActiveRecord::Base
574
580
 
575
581
  if models.first.id.nil?
576
582
  Array(primary_key).each do |c|
577
- if column_names.include?(c) && columns_hash[c].type == :uuid
583
+ if column_names.include?(c) && schema_columns_hash[c].type == :uuid
578
584
  column_names.delete(c)
579
585
  end
580
586
  end
@@ -697,7 +703,11 @@ class ActiveRecord::Base
697
703
  return_obj = if is_validating
698
704
  import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
699
705
  if models
700
- models.each { |m| failed_instances << m if m.errors.any? }
706
+ models.each_with_index do |m, i|
707
+ next unless m.errors.any?
708
+
709
+ failed_instances << (options[:track_validation_failures] ? [i, m] : m)
710
+ end
701
711
  else
702
712
  # create instances for each of our column/value sets
703
713
  arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
@@ -774,7 +784,10 @@ class ActiveRecord::Base
774
784
  def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
775
785
  return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
776
786
 
777
- column_names = column_names.map(&:to_sym)
787
+ column_names = column_names.map do |name|
788
+ original_name = attribute_alias?(name) ? attribute_alias(name) : name
789
+ original_name.to_sym
790
+ end
778
791
  scope_columns, scope_values = scope_attributes.to_a.transpose
779
792
 
780
793
  unless scope_columns.blank?
@@ -786,15 +799,13 @@ class ActiveRecord::Base
786
799
  end
787
800
  end
788
801
 
789
- if finder_needs_type_condition?
790
- unless column_names.include?(inheritance_column.to_sym)
791
- column_names << inheritance_column.to_sym
792
- array_of_attributes.each { |attrs| attrs << sti_name }
793
- end
802
+ if finder_needs_type_condition? && !column_names.include?(inheritance_column.to_sym)
803
+ column_names << inheritance_column.to_sym
804
+ array_of_attributes.each { |attrs| attrs << sti_name }
794
805
  end
795
806
 
796
807
  columns = column_names.each_with_index.map do |name, i|
797
- column = columns_hash[name.to_s]
808
+ column = schema_columns_hash[name.to_s]
798
809
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
799
810
  column
800
811
  end
@@ -848,6 +859,15 @@ class ActiveRecord::Base
848
859
 
849
860
  private
850
861
 
862
+ def associated_options(options, associated_class)
863
+ return options unless options.key?(:recursive_on_duplicate_key_update)
864
+
865
+ table_name = associated_class.arel_table.name.to_sym
866
+ options.merge(
867
+ on_duplicate_key_update: options[:recursive_on_duplicate_key_update][table_name]
868
+ )
869
+ end
870
+
851
871
  def set_attributes_and_mark_clean(models, import_result, timestamps, options)
852
872
  return if models.nil?
853
873
  models -= import_result.failed_instances
@@ -859,13 +879,13 @@ class ActiveRecord::Base
859
879
  model.id = id
860
880
 
861
881
  timestamps.each do |attr, value|
862
- model.send(attr + "=", value) if model.send(attr).nil?
882
+ model.send("#{attr}=", value) if model.send(attr).nil?
863
883
  end
864
884
  end
865
885
  end
866
886
 
867
887
  deserialize_value = lambda do |column, value|
868
- column = columns_hash[column]
888
+ column = schema_columns_hash[column]
869
889
  return value unless column
870
890
  if respond_to?(:type_caster)
871
891
  type = type_for_attribute(column.name)
@@ -932,7 +952,7 @@ class ActiveRecord::Base
932
952
  association = association.target
933
953
  next if association.blank? || model.public_send(column_name).present?
934
954
 
935
- association_primary_key = Array(association_reflection.association_primary_key)[column_index]
955
+ association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
936
956
  model.public_send("#{column_name}=", association.send(association_primary_key))
937
957
  end
938
958
  end
@@ -954,11 +974,23 @@ class ActiveRecord::Base
954
974
 
955
975
  associated_objects_by_class.each_value do |associations|
956
976
  associations.each_value do |associated_records|
957
- associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
977
+ next if associated_records.empty?
978
+
979
+ associated_class = associated_records.first.class
980
+ associated_class.bulk_import(associated_records,
981
+ associated_options(options, associated_class))
958
982
  end
959
983
  end
960
984
  end
961
985
 
986
+ def schema_columns_hash
987
+ if respond_to?(:ignored_columns) && ignored_columns.any?
988
+ connection.schema_cache.columns_hash(table_name)
989
+ else
990
+ columns_hash
991
+ end
992
+ end
993
+
962
994
  # We are eventually going to call Class.import <objects> so we build up a hash
963
995
  # of class => objects to import.
964
996
  def find_associated_objects_for_import(associated_objects_by_class, model)
@@ -979,7 +1011,10 @@ class ActiveRecord::Base
979
1011
 
980
1012
  changed_objects = association.select { |a| a.new_record? || a.changed? }
981
1013
  changed_objects.each do |child|
982
- child.public_send("#{association_reflection.foreign_key}=", model.id)
1014
+ Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
1015
+ child.public_send("#{column}=", Array(model.id)[index])
1016
+ end
1017
+
983
1018
  # For polymorphic associations
984
1019
  association_name = if model.class.respond_to?(:polymorphic_name)
985
1020
  model.class.polymorphic_name
@@ -5,6 +5,7 @@ require 'active_support/core_ext/array'
5
5
  module ActiveRecord::Import
6
6
  class ValueSetTooLargeError < StandardError
7
7
  attr_reader :size
8
+
8
9
  def initialize(msg = "Value set exceeds max size", size = 0)
9
10
  @size = size
10
11
  super(msg)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Import
5
- VERSION = "1.4.1"
5
+ VERSION = "1.8.1"
6
6
  end
7
7
  end
@@ -1,4 +1,3 @@
1
- # rubocop:disable Naming/FileName
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "active_support/lazy_load_hooks"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["ARE_DB"] = "trilogy"
4
+
5
+ if ENV['AR_VERSION'].to_f <= 7.0
6
+ require "activerecord-trilogy-adapter"
7
+ require "trilogy_adapter/connection"
8
+ ActiveRecord::Base.extend TrilogyAdapter::Connection
9
+ end
@@ -8,6 +8,7 @@ common: &common
8
8
  mysql2: &mysql2
9
9
  <<: *common
10
10
  adapter: mysql2
11
+ host: mysql
11
12
 
12
13
  mysql2spatial:
13
14
  <<: *mysql2
@@ -19,6 +20,7 @@ postgresql: &postgresql
19
20
  <<: *common
20
21
  username: postgres
21
22
  adapter: postgresql
23
+ host: postgresql
22
24
  min_messages: warning
23
25
 
24
26
  postresql_makara:
@@ -50,3 +52,8 @@ sqlite3: &sqlite3
50
52
 
51
53
  spatialite:
52
54
  <<: *sqlite3
55
+
56
+ trilogy:
57
+ <<: *common
58
+ adapter: trilogy
59
+ host: mysql
@@ -66,3 +66,7 @@ sqlite3: &sqlite3
66
66
 
67
67
  spatialite:
68
68
  <<: *sqlite3
69
+
70
+ trilogy:
71
+ <<: *common
72
+ adapter: trilogy
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
4
- require File.expand_path(File.dirname(__FILE__) + '/../support/assertions')
5
- require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
3
+ require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
4
+ require File.expand_path("#{File.dirname(__FILE__)}/../support/assertions")
5
+ require File.expand_path("#{File.dirname(__FILE__)}/../support/mysql/import_examples")
6
6
 
7
7
  should_support_mysql_import_functionality
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
4
- require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
+ require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
4
+ require File.expand_path("#{File.dirname(__FILE__)}/../support/postgresql/import_examples")
5
5
 
6
6
  should_support_postgresql_import_functionality
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
4
- require File.expand_path(File.dirname(__FILE__) + '/../support/sqlite3/import_examples')
3
+ require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
4
+ require File.expand_path("#{File.dirname(__FILE__)}/../support/sqlite3/import_examples")
5
5
 
6
6
  should_support_sqlite3_import_functionality
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
4
- require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
+ require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
4
+ require File.expand_path("#{File.dirname(__FILE__)}/../support/postgresql/import_examples")
5
5
 
6
6
  should_support_postgresql_import_functionality
7
7
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Author < ActiveRecord::Base
4
+ if ENV['AR_VERSION'].to_f >= 7.1
5
+ has_many :composite_books, query_constraints: [:id, :author_id], inverse_of: :author
6
+ end
7
+ end