activerecord-import 1.1.0 → 1.4.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.rubocop.yml +74 -8
  4. data/Brewfile +3 -1
  5. data/CHANGELOG.md +38 -3
  6. data/Gemfile +5 -7
  7. data/README.markdown +13 -12
  8. data/Rakefile +2 -0
  9. data/activerecord-import.gemspec +4 -3
  10. data/benchmarks/benchmark.rb +7 -1
  11. data/benchmarks/lib/base.rb +2 -0
  12. data/benchmarks/lib/cli_parser.rb +3 -1
  13. data/benchmarks/lib/float.rb +2 -0
  14. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  15. data/benchmarks/lib/output_to_csv.rb +2 -0
  16. data/benchmarks/lib/output_to_html.rb +4 -2
  17. data/benchmarks/models/test_innodb.rb +2 -0
  18. data/benchmarks/models/test_memory.rb +2 -0
  19. data/benchmarks/models/test_myisam.rb +2 -0
  20. data/benchmarks/schema/mysql2_schema.rb +2 -0
  21. data/gemfiles/4.2.gemfile +2 -0
  22. data/gemfiles/5.0.gemfile +2 -0
  23. data/gemfiles/5.1.gemfile +2 -0
  24. data/gemfiles/5.2.gemfile +2 -0
  25. data/gemfiles/6.0.gemfile +2 -0
  26. data/gemfiles/6.1.gemfile +3 -0
  27. data/gemfiles/7.0.gemfile +4 -0
  28. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  29. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  30. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  32. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  33. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  36. data/lib/activerecord-import/adapters/abstract_adapter.rb +2 -0
  37. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  38. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  39. data/lib/activerecord-import/adapters/mysql_adapter.rb +3 -1
  40. data/lib/activerecord-import/adapters/postgresql_adapter.rb +41 -30
  41. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +8 -8
  42. data/lib/activerecord-import/base.rb +3 -1
  43. data/lib/activerecord-import/import.rb +62 -32
  44. data/lib/activerecord-import/mysql2.rb +2 -0
  45. data/lib/activerecord-import/postgresql.rb +2 -0
  46. data/lib/activerecord-import/sqlite3.rb +2 -0
  47. data/lib/activerecord-import/synchronize.rb +3 -1
  48. data/lib/activerecord-import/value_sets_parser.rb +2 -0
  49. data/lib/activerecord-import/version.rb +3 -1
  50. data/lib/activerecord-import.rb +3 -1
  51. data/test/adapters/jdbcmysql.rb +2 -0
  52. data/test/adapters/jdbcpostgresql.rb +2 -0
  53. data/test/adapters/jdbcsqlite3.rb +2 -0
  54. data/test/adapters/makara_postgis.rb +2 -0
  55. data/test/adapters/mysql2.rb +2 -0
  56. data/test/adapters/mysql2_makara.rb +2 -0
  57. data/test/adapters/mysql2spatial.rb +2 -0
  58. data/test/adapters/postgis.rb +2 -0
  59. data/test/adapters/postgresql.rb +2 -0
  60. data/test/adapters/postgresql_makara.rb +2 -0
  61. data/test/adapters/seamless_database_pool.rb +2 -0
  62. data/test/adapters/spatialite.rb +2 -0
  63. data/test/adapters/sqlite3.rb +2 -0
  64. data/test/{travis → github}/database.yml +3 -1
  65. data/test/import_test.rb +45 -2
  66. data/test/jdbcmysql/import_test.rb +2 -0
  67. data/test/jdbcpostgresql/import_test.rb +2 -0
  68. data/test/jdbcsqlite3/import_test.rb +2 -0
  69. data/test/makara_postgis/import_test.rb +2 -0
  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 +2 -0
  74. data/test/models/book.rb +2 -0
  75. data/test/models/car.rb +2 -0
  76. data/test/models/card.rb +5 -0
  77. data/test/models/chapter.rb +2 -0
  78. data/test/models/customer.rb +8 -0
  79. data/test/models/deck.rb +8 -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 +8 -0
  85. data/test/models/playing_card.rb +4 -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 +2 -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 +2 -0
  97. data/test/mysql2_makara/import_test.rb +2 -0
  98. data/test/mysqlspatial2/import_test.rb +2 -0
  99. data/test/postgis/import_test.rb +2 -0
  100. data/test/postgresql/import_test.rb +2 -0
  101. data/test/schema/generic_schema.rb +33 -0
  102. data/test/schema/jdbcpostgresql_schema.rb +2 -0
  103. data/test/schema/mysql2_schema.rb +2 -0
  104. data/test/schema/postgis_schema.rb +2 -0
  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 +2 -0
  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 +65 -2
  115. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  116. data/test/support/shared_examples/on_duplicate_key_update.rb +2 -0
  117. data/test/support/shared_examples/recursive_import.rb +23 -1
  118. data/test/support/sqlite3/import_examples.rb +2 -1
  119. data/test/synchronize_test.rb +2 -0
  120. data/test/test_helper.rb +19 -2
  121. data/test/value_sets_bytes_parser_test.rb +2 -0
  122. data/test/value_sets_records_parser_test.rb +2 -0
  123. metadata +25 -15
  124. data/.travis.yml +0 -76
  125. data/gemfiles/3.2.gemfile +0 -2
  126. data/gemfiles/4.0.gemfile +0 -2
  127. data/gemfiles/4.1.gemfile +0 -2
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::AbstractAdapter
2
4
  module InstanceMethods
3
5
  def next_value_for_sequence(sequence_name)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "activerecord-import/adapters/mysql_adapter"
2
4
 
3
5
  module ActiveRecord::Import::EMMysql2Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "activerecord-import/adapters/mysql_adapter"
2
4
 
3
5
  module ActiveRecord::Import::Mysql2Adapter
@@ -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
@@ -82,7 +84,7 @@ module ActiveRecord::Import::MysqlAdapter
82
84
  # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
83
85
  # in +args+.
84
86
  def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
85
- sql = ' ON DUPLICATE KEY UPDATE '
87
+ sql = ' ON DUPLICATE KEY UPDATE '.dup
86
88
  arg = args.first
87
89
  locking_column = args.last
88
90
  if arg.is_a?( Array )
@@ -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,7 +8,7 @@ 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
 
@@ -18,47 +20,53 @@ module ActiveRecord::Import::PostgreSQLAdapter
18
20
 
19
21
  sql2insert = base_sql + values.join( ',' ) + post_sql
20
22
 
21
- columns = returning_columns(options)
22
- if columns.blank? || (options[:no_returning] && !options[:recursive])
23
+ selections = returning_selections(options)
24
+ if selections.blank? || (options[:no_returning] && !options[:recursive])
23
25
  insert( sql2insert, *args )
24
26
  else
25
- returned_values = if columns.size > 1
27
+ returned_values = if selections.size > 1
26
28
  # Select composite columns
27
- select_rows( sql2insert, *args )
29
+ db_result = select_all( sql2insert, *args )
30
+ { values: db_result.rows, columns: db_result.columns }
28
31
  else
29
- select_values( sql2insert, *args )
32
+ { values: select_values( sql2insert, *args ) }
30
33
  end
31
34
  clear_query_cache if query_cache_enabled
32
35
  end
33
36
 
34
37
  if options[:returning].blank?
35
- ids = returned_values
38
+ ids = Array(returned_values[:values])
36
39
  elsif options[:primary_key].blank?
37
- results = returned_values
40
+ options[:returning_columns] ||= returned_values[:columns]
41
+ results = Array(returned_values[:values])
38
42
  else
39
43
  # split primary key and returning columns
40
- ids, results = split_ids_and_results(returned_values, columns, options)
44
+ ids, results, options[:returning_columns] = split_ids_and_results(returned_values, options)
41
45
  end
42
46
 
43
47
  ActiveRecord::Import::Result.new([], number_of_inserts, ids, results)
44
48
  end
45
49
 
46
- def split_ids_and_results(values, columns, options)
50
+ def split_ids_and_results( selections, options )
47
51
  ids = []
48
- results = []
52
+ returning_values = []
53
+
54
+ columns = Array(selections[:columns])
55
+ values = Array(selections[:values])
49
56
  id_indexes = Array(options[:primary_key]).map { |key| columns.index(key) }
50
- returning_indexes = Array(options[:returning]).map { |key| columns.index(key) }
57
+ returning_columns = columns.reject.with_index { |_, index| id_indexes.include?(index) }
58
+ returning_indexes = returning_columns.map { |column| columns.index(column) }
51
59
 
52
60
  values.each do |value|
53
61
  value_array = Array(value)
54
- ids << id_indexes.map { |i| value_array[i] }
55
- results << returning_indexes.map { |i| value_array[i] }
62
+ ids << id_indexes.map { |index| value_array[index] }
63
+ returning_values << returning_indexes.map { |index| value_array[index] }
56
64
  end
57
65
 
58
66
  ids.map!(&:first) if id_indexes.size == 1
59
- results.map!(&:first) if returning_indexes.size == 1
67
+ returning_values.map!(&:first) if returning_columns.size == 1
60
68
 
61
- [ids, results]
69
+ [ids, returning_values, returning_columns]
62
70
  end
63
71
 
64
72
  def next_value_for_sequence(sequence_name)
@@ -79,19 +87,24 @@ module ActiveRecord::Import::PostgreSQLAdapter
79
87
 
80
88
  sql += super(table_name, options)
81
89
 
82
- columns = returning_columns(options)
83
- unless columns.blank? || (options[:no_returning] && !options[:recursive])
84
- sql << " RETURNING \"#{columns.join('", "')}\""
90
+ selections = returning_selections(options)
91
+ unless selections.blank? || (options[:no_returning] && !options[:recursive])
92
+ sql << " RETURNING #{selections.join(', ')}"
85
93
  end
86
94
 
87
95
  sql
88
96
  end
89
97
 
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
98
+ def returning_selections(options)
99
+ selections = []
100
+ column_names = Array(options[:model].column_names)
101
+
102
+ selections += Array(options[:primary_key]) if options[:primary_key].present?
103
+ selections += Array(options[:returning]) if options[:returning].present?
104
+
105
+ selections.map do |selection|
106
+ column_names.include?(selection.to_s) ? "\"#{selection}\"" : selection
107
+ end
95
108
  end
96
109
 
97
110
  # Add a column to be updated on duplicate key update
@@ -123,7 +136,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
123
136
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
124
137
  return unless arg.is_a?( Hash )
125
138
 
126
- sql = ' ON CONFLICT '
139
+ sql = ' ON CONFLICT '.dup
127
140
  conflict_target = sql_for_conflict_target( arg )
128
141
 
129
142
  columns = arg.fetch( :columns, [] )
@@ -179,9 +192,9 @@ module ActiveRecord::Import::PostgreSQLAdapter
179
192
  if constraint_name.present?
180
193
  "ON CONSTRAINT #{constraint_name} "
181
194
  elsif conflict_target.present?
182
- '(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
183
- sql << "WHERE #{index_predicate} " if index_predicate
184
- end
195
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
196
+ sql += "WHERE #{index_predicate} " if index_predicate
197
+ sql
185
198
  end
186
199
  end
187
200
 
@@ -203,8 +216,6 @@ module ActiveRecord::Import::PostgreSQLAdapter
203
216
  true
204
217
  end
205
218
 
206
- private
207
-
208
219
  def database_version
209
220
  defined?(postgresql_version) ? postgresql_version : super
210
221
  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
@@ -97,7 +99,7 @@ module ActiveRecord::Import::SQLite3Adapter
97
99
  arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
98
100
  return unless arg.is_a?( Hash )
99
101
 
100
- sql = ' ON CONFLICT '
102
+ sql = ' ON CONFLICT '.dup
101
103
  conflict_target = sql_for_conflict_target( arg )
102
104
 
103
105
  columns = arg.fetch( :columns, [] )
@@ -150,9 +152,9 @@ module ActiveRecord::Import::SQLite3Adapter
150
152
  conflict_target = args[:conflict_target]
151
153
  index_predicate = args[:index_predicate]
152
154
  if conflict_target.present?
153
- '(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
154
- sql << "WHERE #{index_predicate} " if index_predicate
155
- end
155
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
156
+ sql += "WHERE #{index_predicate} " if index_predicate
157
+ sql
156
158
  end
157
159
  end
158
160
 
@@ -166,8 +168,6 @@ module ActiveRecord::Import::SQLite3Adapter
166
168
  exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
167
169
  end
168
170
 
169
- private
170
-
171
171
  def database_version
172
172
  defined?(sqlite_version) ? sqlite_version : super
173
173
  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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "ostruct"
2
4
 
3
5
  module ActiveRecord::Import::ConnectionAdapters; end
@@ -34,7 +36,7 @@ module ActiveRecord::Import #:nodoc:
34
36
  @validate_callbacks = klass._validate_callbacks.dup
35
37
 
36
38
  @validate_callbacks.each_with_index do |callback, i|
37
- filter = callback.raw_filter
39
+ filter = callback.respond_to?(:raw_filter) ? callback.raw_filter : callback.filter
38
40
  next unless filter.class.name =~ /Validations::PresenceValidator/ ||
39
41
  (!@options[:validate_uniqueness] &&
40
42
  filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
@@ -49,13 +51,13 @@ module ActiveRecord::Import #:nodoc:
49
51
  associations = klass.reflect_on_all_associations(:belongs_to)
50
52
  associations.each do |assoc|
51
53
  if (index = attrs.index(assoc.name))
52
- key = assoc.foreign_key.to_sym
54
+ key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
53
55
  attrs[index] = key unless attrs.include?(key)
54
56
  end
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|
@@ -547,7 +549,7 @@ class ActiveRecord::Base
547
549
  alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
548
550
 
549
551
  def import_helper( *args )
550
- options = { validate: true, timestamps: true, track_validation_failures: false }
552
+ options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
551
553
  options.merge!( args.pop ) if args.last.is_a? Hash
552
554
  # making sure that current model's primary key is used
553
555
  options[:primary_key] = primary_key
@@ -734,7 +736,10 @@ class ActiveRecord::Base
734
736
  set_attributes_and_mark_clean(models, return_obj, timestamps, options)
735
737
 
736
738
  # if there are auto-save associations on the models we imported that are new, import them as well
737
- import_associations(models, options.dup) if options[:recursive]
739
+ if options[:recursive]
740
+ options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
741
+ import_associations(models, options.dup.merge(validate: false))
742
+ end
738
743
  end
739
744
 
740
745
  return_obj
@@ -775,21 +780,22 @@ class ActiveRecord::Base
775
780
  unless scope_columns.blank?
776
781
  scope_columns.zip(scope_values).each do |name, value|
777
782
  name_as_sym = name.to_sym
778
- next if column_names.include?(name_as_sym)
779
-
780
- is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
781
- value = Array(value).first if is_sti
782
-
783
+ next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
783
784
  column_names << name_as_sym
784
785
  array_of_attributes.each { |attrs| attrs << value }
785
786
  end
786
787
  end
787
788
 
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
794
+ end
795
+
788
796
  columns = column_names.each_with_index.map do |name, i|
789
797
  column = columns_hash[name.to_s]
790
-
791
798
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
792
-
793
799
  column
794
800
  end
795
801
 
@@ -853,7 +859,7 @@ class ActiveRecord::Base
853
859
  model.id = id
854
860
 
855
861
  timestamps.each do |attr, value|
856
- model.send(attr + "=", value)
862
+ model.send(attr + "=", value) if model.send(attr).nil?
857
863
  end
858
864
  end
859
865
  end
@@ -871,19 +877,28 @@ class ActiveRecord::Base
871
877
  end
872
878
  end
873
879
 
874
- if models.size == import_result.results.size
875
- columns = Array(options[:returning])
876
- single_column = "#{columns.first}=" if columns.size == 1
877
- import_result.results.each_with_index do |result, index|
880
+ set_value = lambda do |model, column, value|
881
+ val = deserialize_value.call(column, value)
882
+ if model.attribute_names.include?(column)
883
+ model.send("#{column}=", val)
884
+ else
885
+ attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
886
+ model.instance_variable_set(:@attributes, attributes)
887
+ end
888
+ end
889
+
890
+ columns = Array(options[:returning_columns])
891
+ results = Array(import_result.results)
892
+ if models.size == results.size
893
+ single_column = columns.first if columns.size == 1
894
+ results.each_with_index do |result, index|
878
895
  model = models[index]
879
896
 
880
897
  if single_column
881
- val = deserialize_value.call(columns.first, result)
882
- model.send(single_column, val)
898
+ set_value.call(model, single_column, result)
883
899
  else
884
900
  columns.each_with_index do |column, col_index|
885
- val = deserialize_value.call(column, result[col_index])
886
- model.send("#{column}=", val)
901
+ set_value.call(model, column, result[col_index])
887
902
  end
888
903
  end
889
904
  end
@@ -907,15 +922,19 @@ class ActiveRecord::Base
907
922
  changed_columns = model.changed
908
923
  association_reflections = model.class.reflect_on_all_associations(:belongs_to)
909
924
  association_reflections.each do |association_reflection|
910
- column_name = association_reflection.foreign_key
911
925
  next if association_reflection.options[:polymorphic]
912
- next if changed_columns.include?(column_name)
913
- association = model.association(association_reflection.name)
914
- association = association.target
915
- next if association.blank? || model.public_send(column_name).present?
916
926
 
917
- association_primary_key = association_reflection.association_primary_key
918
- model.public_send("#{column_name}=", association.send(association_primary_key))
927
+ column_names = Array(association_reflection.foreign_key).map(&:to_s)
928
+ column_names.each_with_index do |column_name, column_index|
929
+ next if changed_columns.include?(column_name)
930
+
931
+ association = model.association(association_reflection.name)
932
+ association = association.target
933
+ next if association.blank? || model.public_send(column_name).present?
934
+
935
+ association_primary_key = Array(association_reflection.association_primary_key)[column_index]
936
+ model.public_send("#{column_name}=", association.send(association_primary_key))
937
+ end
919
938
  end
920
939
  end
921
940
 
@@ -928,8 +947,9 @@ class ActiveRecord::Base
928
947
  associated_objects_by_class = {}
929
948
  models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
930
949
 
931
- # :on_duplicate_key_update and :returning not supported for associations
932
- options.delete(:on_duplicate_key_update)
950
+ # :on_duplicate_key_update only supported for all fields
951
+ options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
952
+ # :returning not supported for associations
933
953
  options.delete(:returning)
934
954
 
935
955
  associated_objects_by_class.each_value do |associations|
@@ -961,8 +981,13 @@ class ActiveRecord::Base
961
981
  changed_objects.each do |child|
962
982
  child.public_send("#{association_reflection.foreign_key}=", model.id)
963
983
  # For polymorphic associations
984
+ association_name = if model.class.respond_to?(:polymorphic_name)
985
+ model.class.polymorphic_name
986
+ else
987
+ model.class.base_class
988
+ end
964
989
  association_reflection.type.try do |type|
965
- child.public_send("#{type}=", model.class.base_class.name)
990
+ child.public_send("#{type}=", association_name)
966
991
  end
967
992
  end
968
993
  associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
@@ -1023,7 +1048,12 @@ class ActiveRecord::Base
1023
1048
  end
1024
1049
 
1025
1050
  # use tz as set in ActiveRecord::Base
1026
- timestamp = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
1051
+ default_timezone = if ActiveRecord.respond_to?(:default_timezone)
1052
+ ActiveRecord.default_timezone
1053
+ else
1054
+ ActiveRecord::Base.default_timezone
1055
+ end
1056
+ timestamp = default_timezone == :utc ? Time.now.utc : Time.now
1027
1057
 
1028
1058
  [:create, :update].each do |action|
1029
1059
  timestamp_columns[action].each do |column|
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord # :nodoc:
2
4
  class Base # :nodoc:
3
5
  # Synchronizes the passed in ActiveRecord instances with data
@@ -39,7 +41,7 @@ module ActiveRecord # :nodoc:
39
41
 
40
42
  next unless matched_instance
41
43
 
42
- instance.send :clear_association_cache
44
+ instance.instance_variable_set :@association_cache, {}
43
45
  instance.send :clear_aggregation_cache if instance.respond_to?(:clear_aggregation_cache, true)
44
46
  instance.instance_variable_set :@attributes, matched_instance.instance_variable_get(:@attributes)
45
47
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/array'
2
4
 
3
5
  module ActiveRecord::Import
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module Import
3
- VERSION = "1.1.0".freeze
5
+ VERSION = "1.4.1"
4
6
  end
5
7
  end
@@ -1,4 +1,6 @@
1
- # rubocop:disable Style/FileName
1
+ # rubocop:disable Naming/FileName
2
+ # frozen_string_literal: true
3
+
2
4
  require "active_support/lazy_load_hooks"
3
5
 
4
6
  ActiveSupport.on_load(:active_record) do
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "jdbcmysql"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "jdbcpostgresql"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "jdbcsqlite3"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "postgis"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "mysql2"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "mysql2_makara"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "mysql2spatial"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "postgis"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "postgresql"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "postgresql"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "seamless_database_pool"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "spatialite"
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV["ARE_DB"] = "sqlite3"
@@ -1,7 +1,8 @@
1
1
  common: &common
2
2
  username: root
3
- password:
3
+ password: root
4
4
  encoding: utf8
5
+ collation: utf8_general_ci
5
6
  host: localhost
6
7
  database: activerecord_import_test
7
8
 
@@ -37,6 +38,7 @@ oracle:
37
38
  postgresql: &postgresql
38
39
  <<: *common
39
40
  username: postgres
41
+ password: postgres
40
42
  adapter: postgresql
41
43
  min_messages: warning
42
44
 
data/test/import_test.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path('../test_helper', __FILE__)
2
4
 
3
5
  describe "#import" do
@@ -159,6 +161,25 @@ describe "#import" do
159
161
  Tag.import columns, values, validate: false
160
162
  end
161
163
  end
164
+
165
+ it "should import models that are required to belong to models with composite primary keys" do
166
+ tag = Tag.create!(tag_id: 1, publisher_id: 1, tag: 'Mystery')
167
+ valid_tag_alias = TagAlias.new(tag_id: tag.tag_id, parent_id: tag.publisher_id, alias: 'Detective')
168
+ invalid_tag_aliases = [
169
+ TagAlias.new(tag_id: nil, parent_id: nil, alias: 'Detective'),
170
+ TagAlias.new(tag_id: tag.tag_id, parent_id: nil, alias: 'Detective'),
171
+ TagAlias.new(tag_id: nil, parent_id: tag.publisher_id, alias: 'Detective'),
172
+ ]
173
+
174
+ assert_difference "TagAlias.count", +1 do
175
+ TagAlias.import [valid_tag_alias]
176
+ end
177
+ invalid_tag_aliases.each do |invalid_tag_alias|
178
+ assert_no_difference "TagAlias.count" do
179
+ TagAlias.import [invalid_tag_alias]
180
+ end
181
+ end
182
+ end
162
183
  end
163
184
  end
164
185
 
@@ -169,7 +190,17 @@ describe "#import" do
169
190
  assert_difference "Dictionary.count", +1 do
170
191
  Dictionary.import dictionaries
171
192
  end
172
- assert_equal "Dictionary", Dictionary.first.type
193
+ assert_equal "Dictionary", Dictionary.last.type
194
+ end
195
+
196
+ it "should import arrays successfully" do
197
+ columns = [:author_name, :title]
198
+ values = [["Noah Webster", "Webster's Dictionary"]]
199
+
200
+ assert_difference "Dictionary.count", +1 do
201
+ Dictionary.import columns, values
202
+ end
203
+ assert_equal "Dictionary", Dictionary.last.type
173
204
  end
174
205
  end
175
206
 
@@ -545,7 +576,11 @@ describe "#import" do
545
576
  context "when the timestamps columns are present" do
546
577
  setup do
547
578
  @existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc, updated_at: 2.years.ago.utc, updated_on: 2.years.ago.utc)
548
- ActiveRecord::Base.default_timezone = :utc
579
+ if ActiveRecord.respond_to?(:default_timezone)
580
+ ActiveRecord.default_timezone = :utc
581
+ else
582
+ ActiveRecord::Base.default_timezone = :utc
583
+ end
549
584
  Timecop.freeze(time) do
550
585
  assert_difference "Book.count", +2 do
551
586
  Book.import %w(title author_name publisher created_at created_on updated_at updated_on), [["LDAP", "Big Bird", "Del Rey", nil, nil, nil, nil], [@existing_book.title, @existing_book.author_name, @existing_book.publisher, @existing_book.created_at, @existing_book.created_on, @existing_book.updated_at, @existing_book.updated_on]]
@@ -661,6 +696,14 @@ describe "#import" do
661
696
  assert_equal [val1, val2], scope.map(&column).sort
662
697
  end
663
698
 
699
+ context "for cards and decks" do
700
+ it "works when the polymorphic name is different than base class name" do
701
+ deck = Deck.create(id: 1, name: 'test')
702
+ deck.cards.import [:id, :deck_type], [[1, 'PlayingCard']]
703
+ assert_equal deck.cards.first.deck_type, "PlayingCard"
704
+ end
705
+ end
706
+
664
707
  it "works importing array of hashes" do
665
708
  scope.import [{ column => val1 }, { column => val2 }]
666
709