activerecord-import 1.1.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
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