activerecord-import 1.4.1 → 1.8.1

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.
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