activerecord-import 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yaml +7 -1
  3. data/.rubocop.yml +74 -8
  4. data/.rubocop_todo.yml +6 -16
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +5 -3
  8. data/README.markdown +8 -6
  9. data/Rakefile +2 -0
  10. data/activerecord-import.gemspec +2 -1
  11. data/benchmarks/benchmark.rb +5 -3
  12. data/benchmarks/lib/base.rb +4 -2
  13. data/benchmarks/lib/cli_parser.rb +4 -2
  14. data/benchmarks/lib/float.rb +2 -0
  15. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  16. data/benchmarks/lib/output_to_csv.rb +2 -0
  17. data/benchmarks/lib/output_to_html.rb +4 -2
  18. data/benchmarks/models/test_innodb.rb +2 -0
  19. data/benchmarks/models/test_memory.rb +2 -0
  20. data/benchmarks/models/test_myisam.rb +2 -0
  21. data/benchmarks/schema/mysql2_schema.rb +2 -0
  22. data/gemfiles/4.2.gemfile +2 -0
  23. data/gemfiles/5.0.gemfile +2 -0
  24. data/gemfiles/5.1.gemfile +2 -0
  25. data/gemfiles/5.2.gemfile +2 -0
  26. data/gemfiles/6.0.gemfile +2 -0
  27. data/gemfiles/6.1.gemfile +2 -0
  28. data/gemfiles/7.0.gemfile +3 -0
  29. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  30. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +2 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  33. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  37. data/lib/activerecord-import/adapters/abstract_adapter.rb +8 -5
  38. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  39. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  40. data/lib/activerecord-import/adapters/mysql_adapter.rb +26 -18
  41. data/lib/activerecord-import/adapters/postgresql_adapter.rb +63 -42
  42. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +33 -25
  43. data/lib/activerecord-import/base.rb +3 -1
  44. data/lib/activerecord-import/import.rb +60 -32
  45. data/lib/activerecord-import/mysql2.rb +2 -0
  46. data/lib/activerecord-import/postgresql.rb +2 -0
  47. data/lib/activerecord-import/sqlite3.rb +2 -0
  48. data/lib/activerecord-import/synchronize.rb +2 -0
  49. data/lib/activerecord-import/value_sets_parser.rb +3 -0
  50. data/lib/activerecord-import/version.rb +3 -1
  51. data/lib/activerecord-import.rb +3 -1
  52. data/test/adapters/jdbcmysql.rb +2 -0
  53. data/test/adapters/jdbcpostgresql.rb +2 -0
  54. data/test/adapters/jdbcsqlite3.rb +2 -0
  55. data/test/adapters/makara_postgis.rb +2 -0
  56. data/test/adapters/mysql2.rb +2 -0
  57. data/test/adapters/mysql2_makara.rb +2 -0
  58. data/test/adapters/mysql2spatial.rb +2 -0
  59. data/test/adapters/postgis.rb +2 -0
  60. data/test/adapters/postgresql.rb +2 -0
  61. data/test/adapters/postgresql_makara.rb +2 -0
  62. data/test/adapters/seamless_database_pool.rb +2 -0
  63. data/test/adapters/spatialite.rb +2 -0
  64. data/test/adapters/sqlite3.rb +2 -0
  65. data/test/import_test.rb +21 -0
  66. data/test/jdbcmysql/import_test.rb +5 -3
  67. data/test/jdbcpostgresql/import_test.rb +4 -2
  68. data/test/jdbcsqlite3/import_test.rb +4 -2
  69. data/test/makara_postgis/import_test.rb +4 -2
  70. data/test/models/account.rb +2 -0
  71. data/test/models/alarm.rb +2 -0
  72. data/test/models/animal.rb +2 -0
  73. data/test/models/bike_maker.rb +3 -0
  74. data/test/models/book.rb +2 -0
  75. data/test/models/car.rb +2 -0
  76. data/test/models/card.rb +2 -0
  77. data/test/models/chapter.rb +2 -0
  78. data/test/models/customer.rb +2 -0
  79. data/test/models/deck.rb +2 -0
  80. data/test/models/dictionary.rb +2 -0
  81. data/test/models/discount.rb +2 -0
  82. data/test/models/end_note.rb +2 -0
  83. data/test/models/group.rb +2 -0
  84. data/test/models/order.rb +2 -0
  85. data/test/models/playing_card.rb +2 -0
  86. data/test/models/promotion.rb +2 -0
  87. data/test/models/question.rb +2 -0
  88. data/test/models/rule.rb +2 -0
  89. data/test/models/tag.rb +3 -0
  90. data/test/models/tag_alias.rb +5 -0
  91. data/test/models/topic.rb +7 -0
  92. data/test/models/user.rb +2 -0
  93. data/test/models/user_token.rb +2 -0
  94. data/test/models/vendor.rb +2 -0
  95. data/test/models/widget.rb +2 -0
  96. data/test/mysql2/import_test.rb +5 -3
  97. data/test/mysql2_makara/import_test.rb +5 -3
  98. data/test/mysqlspatial2/import_test.rb +5 -3
  99. data/test/postgis/import_test.rb +4 -2
  100. data/test/postgresql/import_test.rb +4 -2
  101. data/test/schema/generic_schema.rb +9 -0
  102. data/test/schema/jdbcpostgresql_schema.rb +3 -1
  103. data/test/schema/mysql2_schema.rb +2 -0
  104. data/test/schema/postgis_schema.rb +3 -1
  105. data/test/schema/postgresql_schema.rb +2 -0
  106. data/test/schema/sqlite3_schema.rb +2 -0
  107. data/test/schema/version.rb +2 -0
  108. data/test/sqlite3/import_test.rb +4 -2
  109. data/test/support/active_support/test_case_extensions.rb +2 -0
  110. data/test/support/assertions.rb +2 -0
  111. data/test/support/factories.rb +2 -0
  112. data/test/support/generate.rb +4 -2
  113. data/test/support/mysql/import_examples.rb +2 -1
  114. data/test/support/postgresql/import_examples.rb +40 -1
  115. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  116. data/test/support/shared_examples/on_duplicate_key_update.rb +41 -10
  117. data/test/support/shared_examples/recursive_import.rb +2 -0
  118. data/test/support/sqlite3/import_examples.rb +2 -1
  119. data/test/synchronize_test.rb +2 -0
  120. data/test/test_helper.rb +11 -3
  121. data/test/value_sets_bytes_parser_test.rb +3 -1
  122. data/test/value_sets_records_parser_test.rb +3 -1
  123. metadata +5 -3
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::MysqlAdapter
2
4
  include ActiveRecord::Import::ImportSupport
3
5
  include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
@@ -11,13 +13,14 @@ module ActiveRecord::Import::MysqlAdapter
11
13
  # the number of inserts default
12
14
  number_of_inserts = 0
13
15
 
14
- base_sql, post_sql = if sql.is_a?( String )
15
- [sql, '']
16
- elsif sql.is_a?( Array )
17
- [sql.shift, sql.join( ' ' )]
16
+ base_sql, post_sql = case sql
17
+ when String
18
+ [sql, '']
19
+ when Array
20
+ [sql.shift, sql.join( ' ' )]
18
21
  end
19
22
 
20
- sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
23
+ sql_size = QUERY_OVERHEAD + base_sql.bytesize + post_sql.bytesize
21
24
 
22
25
  # the number of bytes the requested insert statement values will take up
23
26
  values_in_bytes = values.sum(&:bytesize)
@@ -31,7 +34,7 @@ module ActiveRecord::Import::MysqlAdapter
31
34
  max = max_allowed_packet
32
35
 
33
36
  # if we can insert it all as one statement
34
- 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]
35
38
  number_of_inserts += 1
36
39
  sql2insert = base_sql + values.join( ',' ) + post_sql
37
40
  insert( sql2insert, *args )
@@ -83,13 +86,13 @@ module ActiveRecord::Import::MysqlAdapter
83
86
  # in +args+.
84
87
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
85
88
  sql = ' ON DUPLICATE KEY UPDATE '.dup
86
- arg = args.first
87
- locking_column = args.last
88
- if arg.is_a?( Array )
89
- sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arg )
90
- elsif arg.is_a?( Hash )
91
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, arg )
92
- 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
93
96
  sql << arg
94
97
  else
95
98
  raise ArgumentError, "Expected Array or Hash"
@@ -97,19 +100,24 @@ module ActiveRecord::Import::MysqlAdapter
97
100
  sql
98
101
  end
99
102
 
100
- 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:
101
104
  results = arr.map do |column|
102
- 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 )
103
107
  "#{table_name}.#{qc}=VALUES(#{qc})"
104
108
  end
105
109
  increment_locking_column!(table_name, results, locking_column)
106
110
  results.join( ',' )
107
111
  end
108
112
 
109
- 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:
110
114
  results = hsh.map do |column1, column2|
111
- qc1 = quote_column_name( column1 )
112
- 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
+
113
121
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
114
122
  end
115
123
  increment_locking_column!(table_name, results, locking_column)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::PostgreSQLAdapter
2
4
  include ActiveRecord::Import::ImportSupport
3
5
  include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
@@ -6,59 +8,66 @@ module ActiveRecord::Import::PostgreSQLAdapter
6
8
 
7
9
  def insert_many( sql, values, options = {}, *args ) # :nodoc:
8
10
  number_of_inserts = 1
9
- returned_values = []
11
+ returned_values = {}
10
12
  ids = []
11
13
  results = []
12
14
 
13
- base_sql, post_sql = if sql.is_a?( String )
14
- [sql, '']
15
- elsif sql.is_a?( Array )
16
- [sql.shift, sql.join( ' ' )]
15
+ base_sql, post_sql = case sql
16
+ when String
17
+ [sql, '']
18
+ when Array
19
+ [sql.shift, sql.join( ' ' )]
17
20
  end
18
21
 
19
22
  sql2insert = base_sql + values.join( ',' ) + post_sql
20
23
 
21
- columns = returning_columns(options)
22
- if columns.blank? || (options[:no_returning] && !options[:recursive])
24
+ selections = returning_selections(options)
25
+ if selections.blank? || (options[:no_returning] && !options[:recursive])
23
26
  insert( sql2insert, *args )
24
27
  else
25
- returned_values = if columns.size > 1
28
+ returned_values = if selections.size > 1
26
29
  # Select composite columns
27
- select_rows( sql2insert, *args )
30
+ db_result = select_all( sql2insert, *args )
31
+ { values: db_result.rows, columns: db_result.columns }
28
32
  else
29
- select_values( sql2insert, *args )
33
+ { values: select_values( sql2insert, *args ) }
30
34
  end
31
35
  clear_query_cache if query_cache_enabled
32
36
  end
33
37
 
34
38
  if options[:returning].blank?
35
- ids = returned_values
39
+ ids = Array(returned_values[:values])
36
40
  elsif options[:primary_key].blank?
37
- results = returned_values
41
+ options[:returning_columns] ||= returned_values[:columns]
42
+ results = Array(returned_values[:values])
38
43
  else
39
44
  # split primary key and returning columns
40
- ids, results = split_ids_and_results(returned_values, columns, options)
45
+ ids, results, options[:returning_columns] = split_ids_and_results(returned_values, options)
41
46
  end
42
47
 
43
48
  ActiveRecord::Import::Result.new([], number_of_inserts, ids, results)
44
49
  end
45
50
 
46
- def split_ids_and_results(values, columns, options)
51
+ def split_ids_and_results( selections, options )
47
52
  ids = []
48
- results = []
53
+ returning_values = []
54
+
55
+ columns = Array(selections[:columns])
56
+ values = Array(selections[:values])
49
57
  id_indexes = Array(options[:primary_key]).map { |key| columns.index(key) }
50
- returning_indexes = Array(options[:returning]).map { |key| columns.index(key) }
58
+ returning_columns = columns.reject.with_index { |_, index| id_indexes.include?(index) }
59
+ returning_indexes = returning_columns.map { |column| columns.index(column) }
51
60
 
52
61
  values.each do |value|
53
62
  value_array = Array(value)
54
- ids << id_indexes.map { |i| value_array[i] }
55
- results << returning_indexes.map { |i| value_array[i] }
63
+ ids << id_indexes.map { |index| value_array[index] }
64
+ returning_values << returning_indexes.map { |index| value_array[index] }
56
65
  end
57
66
 
58
67
  ids.map!(&:first) if id_indexes.size == 1
59
- results.map!(&:first) if returning_indexes.size == 1
68
+ returning_values.map!(&:first) if returning_columns.size == 1
60
69
 
61
- [ids, results]
70
+ [ids, returning_values, returning_columns]
62
71
  end
63
72
 
64
73
  def next_value_for_sequence(sequence_name)
@@ -79,31 +88,37 @@ module ActiveRecord::Import::PostgreSQLAdapter
79
88
 
80
89
  sql += super(table_name, options)
81
90
 
82
- columns = returning_columns(options)
83
- unless columns.blank? || (options[:no_returning] && !options[:recursive])
84
- sql << " RETURNING \"#{columns.join('", "')}\""
91
+ selections = returning_selections(options)
92
+ unless selections.blank? || (options[:no_returning] && !options[:recursive])
93
+ sql << " RETURNING #{selections.join(', ')}"
85
94
  end
86
95
 
87
96
  sql
88
97
  end
89
98
 
90
- def returning_columns(options)
91
- columns = []
92
- columns += Array(options[:primary_key]) if options[:primary_key].present?
93
- columns |= Array(options[:returning]) if options[:returning].present?
94
- columns
99
+ def returning_selections(options)
100
+ selections = []
101
+ column_names = Array(options[:model].column_names)
102
+
103
+ selections += Array(options[:primary_key]) if options[:primary_key].present?
104
+ selections += Array(options[:returning]) if options[:returning].present?
105
+
106
+ selections.map do |selection|
107
+ column_names.include?(selection.to_s) ? "\"#{selection}\"" : selection
108
+ end
95
109
  end
96
110
 
97
111
  # Add a column to be updated on duplicate key update
98
112
  def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
99
113
  arg = options[:on_duplicate_key_update]
100
- if arg.is_a?( Hash )
114
+ case arg
115
+ when Hash
101
116
  columns = arg.fetch( :columns ) { arg[:columns] = [] }
102
117
  case columns
103
118
  when Array then columns << column.to_sym unless columns.include?( column.to_sym )
104
119
  when Hash then columns[column.to_sym] = column.to_sym
105
120
  end
106
- elsif arg.is_a?( Array )
121
+ when Array
107
122
  arg << column.to_sym unless arg.include?( column.to_sym )
108
123
  end
109
124
  end
@@ -119,7 +134,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
119
134
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
120
135
  # in +args+.
121
136
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
122
- arg, primary_key, locking_column = args
137
+ arg, model, primary_key, locking_column = args
123
138
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
124
139
  return unless arg.is_a?( Hash )
125
140
 
@@ -138,11 +153,12 @@ module ActiveRecord::Import::PostgreSQLAdapter
138
153
  end
139
154
 
140
155
  sql << "#{conflict_target}DO UPDATE SET "
141
- if columns.is_a?( Array )
142
- sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
143
- elsif columns.is_a?( Hash )
144
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
145
- 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
146
162
  sql << columns
147
163
  else
148
164
  raise ArgumentError, 'Expected :columns to be an Array or Hash'
@@ -153,19 +169,24 @@ module ActiveRecord::Import::PostgreSQLAdapter
153
169
  sql
154
170
  end
155
171
 
156
- 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:
157
173
  results = arr.map do |column|
158
- 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 )
159
176
  "#{qc}=EXCLUDED.#{qc}"
160
177
  end
161
178
  increment_locking_column!(table_name, results, locking_column)
162
179
  results.join( ',' )
163
180
  end
164
181
 
165
- 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:
166
183
  results = hsh.map do |column1, column2|
167
- qc1 = quote_column_name( column1 )
168
- 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
+
169
190
  "#{qc1}=EXCLUDED.#{qc2}"
170
191
  end
171
192
  increment_locking_column!(table_name, results, locking_column)
@@ -179,7 +200,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
179
200
  if constraint_name.present?
180
201
  "ON CONSTRAINT #{constraint_name} "
181
202
  elsif conflict_target.present?
182
- sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
203
+ sql = "(#{Array( conflict_target ).reject( &:blank? ).join( ', ' )}) "
183
204
  sql += "WHERE #{index_predicate} " if index_predicate
184
205
  sql
185
206
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::SQLite3Adapter
2
4
  include ActiveRecord::Import::ImportSupport
3
5
  include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
6
 
5
- MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
6
- MIN_VERSION_FOR_UPSERT = "3.24.0".freeze
7
+ MIN_VERSION_FOR_IMPORT = "3.7.11"
8
+ MIN_VERSION_FOR_UPSERT = "3.24.0"
7
9
  SQLITE_LIMIT_COMPOUND_SELECT = 500
8
10
 
9
11
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
@@ -22,10 +24,11 @@ module ActiveRecord::Import::SQLite3Adapter
22
24
  def insert_many( sql, values, _options = {}, *args ) # :nodoc:
23
25
  number_of_inserts = 0
24
26
 
25
- base_sql, post_sql = if sql.is_a?( String )
26
- [sql, '']
27
- elsif sql.is_a?( Array )
28
- [sql.shift, sql.join( ' ' )]
27
+ base_sql, post_sql = case sql
28
+ when String
29
+ [sql, '']
30
+ when Array
31
+ [sql.shift, sql.join( ' ' )]
29
32
  end
30
33
 
31
34
  value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
@@ -54,11 +57,9 @@ module ActiveRecord::Import::SQLite3Adapter
54
57
  def post_sql_statements( table_name, options ) # :nodoc:
55
58
  sql = []
56
59
 
57
- if supports_on_duplicate_key_update?
58
- # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
59
- if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update]
60
- sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
61
- 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] )
62
63
  end
63
64
 
64
65
  sql + super
@@ -71,13 +72,14 @@ module ActiveRecord::Import::SQLite3Adapter
71
72
  # Add a column to be updated on duplicate key update
72
73
  def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
73
74
  arg = options[:on_duplicate_key_update]
74
- if arg.is_a?( Hash )
75
+ case arg
76
+ when Hash
75
77
  columns = arg.fetch( :columns ) { arg[:columns] = [] }
76
78
  case columns
77
79
  when Array then columns << column.to_sym unless columns.include?( column.to_sym )
78
80
  when Hash then columns[column.to_sym] = column.to_sym
79
81
  end
80
- elsif arg.is_a?( Array )
82
+ when Array
81
83
  arg << column.to_sym unless arg.include?( column.to_sym )
82
84
  end
83
85
  end
@@ -93,7 +95,7 @@ module ActiveRecord::Import::SQLite3Adapter
93
95
  # Returns a generated ON CONFLICT DO UPDATE statement given the passed
94
96
  # in +args+.
95
97
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
96
- arg, primary_key, locking_column = args
98
+ arg, model, primary_key, locking_column = args
97
99
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
98
100
  return unless arg.is_a?( Hash )
99
101
 
@@ -112,11 +114,12 @@ module ActiveRecord::Import::SQLite3Adapter
112
114
  end
113
115
 
114
116
  sql << "#{conflict_target}DO UPDATE SET "
115
- if columns.is_a?( Array )
116
- sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
117
- elsif columns.is_a?( Hash )
118
- sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
119
- 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
120
123
  sql << columns
121
124
  else
122
125
  raise ArgumentError, 'Expected :columns to be an Array or Hash'
@@ -127,19 +130,24 @@ module ActiveRecord::Import::SQLite3Adapter
127
130
  sql
128
131
  end
129
132
 
130
- 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:
131
134
  results = arr.map do |column|
132
- 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 )
133
137
  "#{qc}=EXCLUDED.#{qc}"
134
138
  end
135
139
  increment_locking_column!(table_name, results, locking_column)
136
140
  results.join( ',' )
137
141
  end
138
142
 
139
- 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:
140
144
  results = hsh.map do |column1, column2|
141
- qc1 = quote_column_name( column1 )
142
- 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
+
143
151
  "#{qc1}=EXCLUDED.#{qc2}"
144
152
  end
145
153
  increment_locking_column!(table_name, results, locking_column)
@@ -150,7 +158,7 @@ module ActiveRecord::Import::SQLite3Adapter
150
158
  conflict_target = args[:conflict_target]
151
159
  index_predicate = args[:index_predicate]
152
160
  if conflict_target.present?
153
- sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
161
+ sql = "(#{Array( conflict_target ).reject( &:blank? ).join( ', ' )}) "
154
162
  sql += "WHERE #{index_predicate} " if index_predicate
155
163
  sql
156
164
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pathname"
2
4
  require "active_record"
3
5
  require "active_record/version"
4
6
 
5
7
  module ActiveRecord::Import
6
- ADAPTER_PATH = "activerecord-import/active_record/adapters".freeze
8
+ ADAPTER_PATH = "activerecord-import/active_record/adapters"
7
9
 
8
10
  def self.base_adapter(adapter)
9
11
  case adapter
@@ -1,18 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ostruct"
2
4
 
3
5
  module ActiveRecord::Import::ConnectionAdapters; end
4
6
 
5
- module ActiveRecord::Import #:nodoc:
7
+ module ActiveRecord::Import # :nodoc:
6
8
  Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
7
9
 
8
- module ImportSupport #:nodoc:
9
- def supports_import? #:nodoc:
10
+ module ImportSupport # :nodoc:
11
+ def supports_import? # :nodoc:
10
12
  true
11
13
  end
12
14
  end
13
15
 
14
- module OnDuplicateKeyUpdateSupport #:nodoc:
15
- def supports_on_duplicate_key_update? #:nodoc:
16
+ module OnDuplicateKeyUpdateSupport # :nodoc:
17
+ def supports_on_duplicate_key_update? # :nodoc:
16
18
  true
17
19
  end
18
20
  end
@@ -55,7 +57,7 @@ module ActiveRecord::Import #:nodoc:
55
57
  end
56
58
  end
57
59
 
58
- filter.instance_variable_set(:@attributes, attrs)
60
+ filter.instance_variable_set(:@attributes, attrs.flatten)
59
61
 
60
62
  if @validate_callbacks.respond_to?(:chain, true)
61
63
  @validate_callbacks.send(:chain).tap do |chain|
@@ -71,7 +73,7 @@ module ActiveRecord::Import #:nodoc:
71
73
  end
72
74
 
73
75
  def valid_model?(model)
74
- init_validations(model.class) unless model.class == @validator_class
76
+ init_validations(model.class) unless model.instance_of?(@validator_class)
75
77
 
76
78
  validation_context = @options[:validate_with_context]
77
79
  validation_context ||= (model.new_record? ? :create : :update)
@@ -83,7 +85,11 @@ module ActiveRecord::Import #:nodoc:
83
85
 
84
86
  model.run_callbacks(:validation) do
85
87
  if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
86
- runner = @validate_callbacks.compile
88
+ runner = if @validate_callbacks.method(:compile).arity == 0
89
+ @validate_callbacks.compile
90
+ else # ActiveRecord >= 7.1
91
+ @validate_callbacks.compile(nil)
92
+ end
87
93
  env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
88
94
  if runner.respond_to?(:call) # ActiveRecord < 5.1
89
95
  runner.call(env)
@@ -163,7 +169,7 @@ class ActiveRecord::Associations::CollectionAssociation
163
169
  m.public_send "#{reflection.type}=", owner.class.name if reflection.type
164
170
  end
165
171
 
166
- return model_klass.bulk_import column_names, models, options
172
+ model_klass.bulk_import column_names, models, options
167
173
 
168
174
  # supports array of hash objects
169
175
  elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
@@ -202,11 +208,11 @@ class ActiveRecord::Associations::CollectionAssociation
202
208
  end
203
209
  end
204
210
 
205
- return model_klass.bulk_import column_names, array_of_attributes, options
211
+ model_klass.bulk_import column_names, array_of_attributes, options
206
212
 
207
213
  # supports empty array
208
214
  elsif args.last.is_a?( Array ) && args.last.empty?
209
- return ActiveRecord::Import::Result.new([], 0, [])
215
+ ActiveRecord::Import::Result.new([], 0, [])
210
216
 
211
217
  # supports 2-element array and array
212
218
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
@@ -237,7 +243,7 @@ class ActiveRecord::Associations::CollectionAssociation
237
243
  end
238
244
  end
239
245
 
240
- return model_klass.bulk_import column_names, array_of_attributes, options
246
+ model_klass.bulk_import column_names, array_of_attributes, options
241
247
  else
242
248
  raise ArgumentError, "Invalid arguments!"
243
249
  end
@@ -547,7 +553,7 @@ class ActiveRecord::Base
547
553
  alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
548
554
 
549
555
  def import_helper( *args )
550
- options = { validate: true, timestamps: true, track_validation_failures: false }
556
+ options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
551
557
  options.merge!( args.pop ) if args.last.is_a? Hash
552
558
  # making sure that current model's primary key is used
553
559
  options[:primary_key] = primary_key
@@ -572,7 +578,7 @@ class ActiveRecord::Base
572
578
 
573
579
  if models.first.id.nil?
574
580
  Array(primary_key).each do |c|
575
- if column_names.include?(c) && columns_hash[c].type == :uuid
581
+ if column_names.include?(c) && schema_columns_hash[c].type == :uuid
576
582
  column_names.delete(c)
577
583
  end
578
584
  end
@@ -695,7 +701,11 @@ class ActiveRecord::Base
695
701
  return_obj = if is_validating
696
702
  import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
697
703
  if models
698
- models.each { |m| failed_instances << m if m.errors.any? }
704
+ models.each_with_index do |m, i|
705
+ next unless m.errors.any?
706
+
707
+ failed_instances << (options[:track_validation_failures] ? [i, m] : m)
708
+ end
699
709
  else
700
710
  # create instances for each of our column/value sets
701
711
  arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
@@ -772,7 +782,10 @@ class ActiveRecord::Base
772
782
  def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
773
783
  return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
774
784
 
775
- column_names = column_names.map(&:to_sym)
785
+ column_names = column_names.map do |name|
786
+ original_name = attribute_alias?(name) ? attribute_alias(name) : name
787
+ original_name.to_sym
788
+ end
776
789
  scope_columns, scope_values = scope_attributes.to_a.transpose
777
790
 
778
791
  unless scope_columns.blank?
@@ -784,15 +797,13 @@ class ActiveRecord::Base
784
797
  end
785
798
  end
786
799
 
787
- if finder_needs_type_condition?
788
- unless column_names.include?(inheritance_column.to_sym)
789
- column_names << inheritance_column.to_sym
790
- array_of_attributes.each { |attrs| attrs << sti_name }
791
- end
800
+ if finder_needs_type_condition? && !column_names.include?(inheritance_column.to_sym)
801
+ column_names << inheritance_column.to_sym
802
+ array_of_attributes.each { |attrs| attrs << sti_name }
792
803
  end
793
804
 
794
805
  columns = column_names.each_with_index.map do |name, i|
795
- column = columns_hash[name.to_s]
806
+ column = schema_columns_hash[name.to_s]
796
807
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
797
808
  column
798
809
  end
@@ -857,13 +868,13 @@ class ActiveRecord::Base
857
868
  model.id = id
858
869
 
859
870
  timestamps.each do |attr, value|
860
- model.send(attr + "=", value) if model.send(attr).nil?
871
+ model.send("#{attr}=", value) if model.send(attr).nil?
861
872
  end
862
873
  end
863
874
  end
864
875
 
865
876
  deserialize_value = lambda do |column, value|
866
- column = columns_hash[column]
877
+ column = schema_columns_hash[column]
867
878
  return value unless column
868
879
  if respond_to?(:type_caster)
869
880
  type = type_for_attribute(column.name)
@@ -875,19 +886,28 @@ class ActiveRecord::Base
875
886
  end
876
887
  end
877
888
 
878
- if models.size == import_result.results.size
879
- columns = Array(options[:returning])
880
- single_column = "#{columns.first}=" if columns.size == 1
881
- import_result.results.each_with_index do |result, index|
889
+ set_value = lambda do |model, column, value|
890
+ val = deserialize_value.call(column, value)
891
+ if model.attribute_names.include?(column)
892
+ model.send("#{column}=", val)
893
+ else
894
+ attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
895
+ model.instance_variable_set(:@attributes, attributes)
896
+ end
897
+ end
898
+
899
+ columns = Array(options[:returning_columns])
900
+ results = Array(import_result.results)
901
+ if models.size == results.size
902
+ single_column = columns.first if columns.size == 1
903
+ results.each_with_index do |result, index|
882
904
  model = models[index]
883
905
 
884
906
  if single_column
885
- val = deserialize_value.call(columns.first, result)
886
- model.send(single_column, val)
907
+ set_value.call(model, single_column, result)
887
908
  else
888
909
  columns.each_with_index do |column, col_index|
889
- val = deserialize_value.call(column, result[col_index])
890
- model.send("#{column}=", val)
910
+ set_value.call(model, column, result[col_index])
891
911
  end
892
912
  end
893
913
  end
@@ -948,6 +968,14 @@ class ActiveRecord::Base
948
968
  end
949
969
  end
950
970
 
971
+ def schema_columns_hash
972
+ @schema_columns_hash ||= if respond_to?(:ignored_columns) && ignored_columns.any?
973
+ connection.schema_cache.columns_hash(table_name)
974
+ else
975
+ columns_hash
976
+ end
977
+ end
978
+
951
979
  # We are eventually going to call Class.import <objects> so we build up a hash
952
980
  # of class => objects to import.
953
981
  def find_associated_objects_for_import(associated_objects_by_class, model)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  warn <<-MSG
2
4
  [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
5
  is deprecated. Update to autorequire using 'require "activerecord-import"'. See
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  warn <<-MSG
2
4
  [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
5
  is deprecated. Update to autorequire using 'require "activerecord-import"'. See
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  warn <<-MSG
2
4
  [DEPRECATION] loading activerecord-import via 'require "activerecord-import/<adapter-name>"'
3
5
  is deprecated. Update to autorequire using 'require "activerecord-import"'. See