activerecord-import 1.0.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yaml +159 -0
  3. data/.gitignore +5 -0
  4. data/.rubocop.yml +76 -7
  5. data/.rubocop_todo.yml +10 -16
  6. data/Brewfile +3 -1
  7. data/CHANGELOG.md +143 -3
  8. data/Dockerfile +23 -0
  9. data/Gemfile +28 -24
  10. data/LICENSE +21 -56
  11. data/README.markdown +83 -27
  12. data/Rakefile +3 -0
  13. data/activerecord-import.gemspec +10 -5
  14. data/benchmarks/benchmark.rb +10 -6
  15. data/benchmarks/lib/base.rb +10 -5
  16. data/benchmarks/lib/cli_parser.rb +10 -6
  17. data/benchmarks/lib/float.rb +2 -0
  18. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  19. data/benchmarks/lib/output_to_csv.rb +2 -0
  20. data/benchmarks/lib/output_to_html.rb +4 -2
  21. data/benchmarks/models/test_innodb.rb +2 -0
  22. data/benchmarks/models/test_memory.rb +2 -0
  23. data/benchmarks/models/test_myisam.rb +2 -0
  24. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  25. data/docker-compose.yml +34 -0
  26. data/gemfiles/5.2.gemfile +2 -0
  27. data/gemfiles/6.0.gemfile +3 -0
  28. data/gemfiles/6.1.gemfile +4 -1
  29. data/gemfiles/7.0.gemfile +4 -0
  30. data/gemfiles/7.1.gemfile +3 -0
  31. data/gemfiles/7.2.gemfile +3 -0
  32. data/gemfiles/8.0.gemfile +3 -0
  33. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  35. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  38. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  39. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  40. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  41. data/lib/activerecord-import/active_record/adapters/trilogy_adapter.rb +8 -0
  42. data/lib/activerecord-import/adapters/abstract_adapter.rb +9 -6
  43. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  44. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  45. data/lib/activerecord-import/adapters/mysql_adapter.rb +30 -21
  46. data/lib/activerecord-import/adapters/postgresql_adapter.rb +68 -48
  47. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +37 -30
  48. data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
  49. data/lib/activerecord-import/base.rb +3 -1
  50. data/lib/activerecord-import/import.rb +160 -58
  51. data/lib/activerecord-import/synchronize.rb +3 -1
  52. data/lib/activerecord-import/value_sets_parser.rb +5 -0
  53. data/lib/activerecord-import/version.rb +3 -1
  54. data/lib/activerecord-import.rb +2 -1
  55. data/test/adapters/jdbcmysql.rb +2 -0
  56. data/test/adapters/jdbcpostgresql.rb +2 -0
  57. data/test/adapters/jdbcsqlite3.rb +2 -0
  58. data/test/adapters/makara_postgis.rb +2 -0
  59. data/test/adapters/mysql2.rb +2 -0
  60. data/test/adapters/mysql2_makara.rb +2 -0
  61. data/test/adapters/mysql2spatial.rb +2 -0
  62. data/test/adapters/postgis.rb +2 -0
  63. data/test/adapters/postgresql.rb +2 -0
  64. data/test/adapters/postgresql_makara.rb +2 -0
  65. data/test/adapters/seamless_database_pool.rb +2 -0
  66. data/test/adapters/spatialite.rb +2 -0
  67. data/test/adapters/sqlite3.rb +2 -0
  68. data/test/adapters/trilogy.rb +9 -0
  69. data/test/database.yml.sample +7 -0
  70. data/test/{travis → github}/database.yml +9 -3
  71. data/test/import_test.rb +108 -41
  72. data/test/jdbcmysql/import_test.rb +5 -3
  73. data/test/jdbcpostgresql/import_test.rb +4 -2
  74. data/test/jdbcsqlite3/import_test.rb +4 -2
  75. data/test/makara_postgis/import_test.rb +4 -2
  76. data/test/models/account.rb +2 -0
  77. data/test/models/alarm.rb +2 -0
  78. data/test/models/animal.rb +8 -0
  79. data/test/models/author.rb +9 -0
  80. data/test/models/bike_maker.rb +3 -0
  81. data/test/models/book.rb +12 -3
  82. data/test/models/car.rb +2 -0
  83. data/test/models/card.rb +5 -0
  84. data/test/models/chapter.rb +2 -0
  85. data/test/models/composite_book.rb +19 -0
  86. data/test/models/composite_chapter.rb +12 -0
  87. data/test/models/customer.rb +18 -0
  88. data/test/models/deck.rb +8 -0
  89. data/test/models/dictionary.rb +2 -0
  90. data/test/models/discount.rb +2 -0
  91. data/test/models/end_note.rb +2 -0
  92. data/test/models/group.rb +2 -0
  93. data/test/models/order.rb +17 -0
  94. data/test/models/playing_card.rb +4 -0
  95. data/test/models/promotion.rb +2 -0
  96. data/test/models/question.rb +2 -0
  97. data/test/models/rule.rb +2 -0
  98. data/test/models/tag.rb +9 -1
  99. data/test/models/tag_alias.rb +11 -0
  100. data/test/models/topic.rb +8 -0
  101. data/test/models/user.rb +2 -0
  102. data/test/models/user_token.rb +2 -0
  103. data/test/models/vendor.rb +2 -0
  104. data/test/models/widget.rb +12 -3
  105. data/test/mysql2/import_test.rb +5 -3
  106. data/test/mysql2_makara/import_test.rb +5 -3
  107. data/test/mysqlspatial2/import_test.rb +5 -3
  108. data/test/postgis/import_test.rb +4 -2
  109. data/test/postgresql/import_test.rb +4 -2
  110. data/test/schema/generic_schema.rb +37 -1
  111. data/test/schema/jdbcpostgresql_schema.rb +3 -1
  112. data/test/schema/mysql2_schema.rb +2 -0
  113. data/test/schema/postgis_schema.rb +3 -1
  114. data/test/schema/postgresql_schema.rb +38 -4
  115. data/test/schema/sqlite3_schema.rb +2 -0
  116. data/test/schema/version.rb +2 -0
  117. data/test/sqlite3/import_test.rb +4 -2
  118. data/test/support/active_support/test_case_extensions.rb +3 -5
  119. data/test/support/assertions.rb +2 -0
  120. data/test/support/factories.rb +2 -0
  121. data/test/support/generate.rb +4 -2
  122. data/test/support/mysql/import_examples.rb +7 -8
  123. data/test/support/postgresql/import_examples.rb +121 -53
  124. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  125. data/test/support/shared_examples/on_duplicate_key_update.rb +69 -10
  126. data/test/support/shared_examples/recursive_import.rb +137 -1
  127. data/test/support/sqlite3/import_examples.rb +2 -1
  128. data/test/synchronize_test.rb +2 -0
  129. data/test/test_helper.rb +38 -24
  130. data/test/trilogy/import_test.rb +7 -0
  131. data/test/value_sets_bytes_parser_test.rb +3 -1
  132. data/test/value_sets_records_parser_test.rb +3 -1
  133. metadata +46 -22
  134. data/.travis.yml +0 -74
  135. data/gemfiles/3.2.gemfile +0 -2
  136. data/gemfiles/4.0.gemfile +0 -2
  137. data/gemfiles/4.1.gemfile +0 -2
  138. data/gemfiles/4.2.gemfile +0 -2
  139. data/gemfiles/5.0.gemfile +0 -2
  140. data/gemfiles/5.1.gemfile +0 -2
  141. data/lib/activerecord-import/mysql2.rb +0 -7
  142. data/lib/activerecord-import/postgresql.rb +0 -7
  143. data/lib/activerecord-import/sqlite3.rb +0 -7
@@ -1,12 +1,18 @@
1
- require "ostruct"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord::Import::ConnectionAdapters; end
4
4
 
5
- module ActiveRecord::Import #:nodoc:
5
+ module ActiveRecord::Import # :nodoc:
6
6
  Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
7
7
 
8
- module ImportSupport #:nodoc:
9
- def supports_import? #:nodoc:
8
+ module ImportSupport # :nodoc:
9
+ def supports_import? # :nodoc:
10
+ true
11
+ end
12
+ end
13
+
14
+ module OnDuplicateKeyUpdateSupport # :nodoc:
15
+ def supports_on_duplicate_key_update? # :nodoc:
10
16
  true
11
17
  end
12
18
  end
@@ -28,7 +34,7 @@ module ActiveRecord::Import #:nodoc:
28
34
  @validate_callbacks = klass._validate_callbacks.dup
29
35
 
30
36
  @validate_callbacks.each_with_index do |callback, i|
31
- filter = callback.raw_filter
37
+ filter = callback.respond_to?(:raw_filter) ? callback.raw_filter : callback.filter
32
38
  next unless filter.class.name =~ /Validations::PresenceValidator/ ||
33
39
  (!@options[:validate_uniqueness] &&
34
40
  filter.is_a?(ActiveRecord::Validations::UniquenessValidator))
@@ -43,17 +49,18 @@ module ActiveRecord::Import #:nodoc:
43
49
  associations = klass.reflect_on_all_associations(:belongs_to)
44
50
  associations.each do |assoc|
45
51
  if (index = attrs.index(assoc.name))
46
- key = assoc.foreign_key.to_sym
52
+ key = assoc.foreign_key.is_a?(Array) ? assoc.foreign_key.map(&:to_sym) : assoc.foreign_key.to_sym
47
53
  attrs[index] = key unless attrs.include?(key)
48
54
  end
49
55
  end
50
56
  end
51
57
 
52
- filter.instance_variable_set(:@attributes, attrs)
58
+ filter.instance_variable_set(:@attributes, attrs.flatten)
53
59
 
54
60
  if @validate_callbacks.respond_to?(:chain, true)
55
61
  @validate_callbacks.send(:chain).tap do |chain|
56
62
  callback.instance_variable_set(:@filter, filter)
63
+ callback.instance_variable_set(:@compiled, nil)
57
64
  chain[i] = callback
58
65
  end
59
66
  else
@@ -65,7 +72,7 @@ module ActiveRecord::Import #:nodoc:
65
72
  end
66
73
 
67
74
  def valid_model?(model)
68
- init_validations(model.class) unless model.class == @validator_class
75
+ init_validations(model.class) unless model.instance_of?(@validator_class)
69
76
 
70
77
  validation_context = @options[:validate_with_context]
71
78
  validation_context ||= (model.new_record? ? :create : :update)
@@ -77,11 +84,15 @@ module ActiveRecord::Import #:nodoc:
77
84
 
78
85
  model.run_callbacks(:validation) do
79
86
  if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
80
- runner = @validate_callbacks.compile
87
+ runner = if @validate_callbacks.method(:compile).arity == 0
88
+ @validate_callbacks.compile
89
+ else # ActiveRecord >= 7.1
90
+ @validate_callbacks.compile(nil)
91
+ end
81
92
  env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
82
93
  if runner.respond_to?(:call) # ActiveRecord < 5.1
83
94
  runner.call(env)
84
- else # ActiveRecord 5.1
95
+ else # ActiveRecord >= 5.1
85
96
  # Note that this is a gross simplification of ActiveSupport::Callbacks#run_callbacks.
86
97
  # It's technically possible for there to exist an "around" callback in the
87
98
  # :validate chain, but this would be an aberration, since Rails doesn't define
@@ -94,7 +105,8 @@ module ActiveRecord::Import #:nodoc:
94
105
  # no real-world use case for it.
95
106
  raise "The :validate callback chain contains an 'around' callback, which is unsupported" unless runner.final?
96
107
  runner.invoke_before(env)
97
- runner.invoke_after(env)
108
+ # Ensure a truthy value is returned. ActiveRecord < 7.2 always returned an array.
109
+ runner.invoke_after(env) || []
98
110
  end
99
111
  elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
100
112
  model.instance_eval @validate_callbacks.compile
@@ -157,7 +169,7 @@ class ActiveRecord::Associations::CollectionAssociation
157
169
  m.public_send "#{reflection.type}=", owner.class.name if reflection.type
158
170
  end
159
171
 
160
- return model_klass.bulk_import column_names, models, options
172
+ model_klass.bulk_import column_names, models, options
161
173
 
162
174
  # supports array of hash objects
163
175
  elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
@@ -196,11 +208,11 @@ class ActiveRecord::Associations::CollectionAssociation
196
208
  end
197
209
  end
198
210
 
199
- return model_klass.bulk_import column_names, array_of_attributes, options
211
+ model_klass.bulk_import column_names, array_of_attributes, options
200
212
 
201
213
  # supports empty array
202
214
  elsif args.last.is_a?( Array ) && args.last.empty?
203
- return ActiveRecord::Import::Result.new([], 0, [])
215
+ ActiveRecord::Import::Result.new([], 0, [])
204
216
 
205
217
  # supports 2-element array and array
206
218
  elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
@@ -231,7 +243,7 @@ class ActiveRecord::Associations::CollectionAssociation
231
243
  end
232
244
  end
233
245
 
234
- return model_klass.bulk_import column_names, array_of_attributes, options
246
+ model_klass.bulk_import column_names, array_of_attributes, options
235
247
  else
236
248
  raise ArgumentError, "Invalid arguments!"
237
249
  end
@@ -241,8 +253,9 @@ end
241
253
 
242
254
  module ActiveRecord::Import::Connection
243
255
  def establish_connection(args = nil)
244
- super(args)
256
+ conn = super(args)
245
257
  ActiveRecord::Import.load_from_connection_pool connection_pool
258
+ conn
246
259
  end
247
260
  end
248
261
 
@@ -540,11 +553,11 @@ class ActiveRecord::Base
540
553
  alias import! bulk_import! unless ActiveRecord::Base.respond_to? :import!
541
554
 
542
555
  def import_helper( *args )
543
- options = { validate: true, timestamps: true }
556
+ options = { model: self, validate: true, timestamps: true, track_validation_failures: false }
544
557
  options.merge!( args.pop ) if args.last.is_a? Hash
545
558
  # making sure that current model's primary key is used
546
559
  options[:primary_key] = primary_key
547
- options[:locking_column] = locking_column if attribute_names.include?(locking_column)
560
+ options[:locking_column] = locking_column if locking_enabled?
548
561
 
549
562
  is_validating = options[:validate_with_context].present? ? true : options[:validate]
550
563
  validator = ActiveRecord::Import::Validator.new(self, options)
@@ -565,7 +578,7 @@ class ActiveRecord::Base
565
578
 
566
579
  if models.first.id.nil?
567
580
  Array(primary_key).each do |c|
568
- if column_names.include?(c) && columns_hash[c].type == :uuid
581
+ if column_names.include?(c) && schema_columns_hash[c].type == :uuid
569
582
  column_names.delete(c)
570
583
  end
571
584
  end
@@ -575,7 +588,7 @@ class ActiveRecord::Base
575
588
  if respond_to?(:timestamp_attributes_for_update, true)
576
589
  send(:timestamp_attributes_for_update).map(&:to_sym)
577
590
  else
578
- new.send(:timestamp_attributes_for_update_in_model)
591
+ allocate.send(:timestamp_attributes_for_update_in_model)
579
592
  end
580
593
  end
581
594
 
@@ -688,7 +701,11 @@ class ActiveRecord::Base
688
701
  return_obj = if is_validating
689
702
  import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
690
703
  if models
691
- 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
692
709
  else
693
710
  # create instances for each of our column/value sets
694
711
  arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
@@ -696,14 +713,18 @@ class ActiveRecord::Base
696
713
  # keep track of the instance and the position it is currently at. if this fails
697
714
  # validation we'll use the index to remove it from the array_of_attributes
698
715
  arr.each_with_index do |hsh, i|
699
- model = new
700
- hsh.each_pair { |k, v| model[k] = v }
716
+ # utilize block initializer syntax to prevent failure when 'mass_assignment_sanitizer = :strict'
717
+ model = new do |m|
718
+ hsh.each_pair { |k, v| m[k] = v }
719
+ end
720
+
701
721
  next if validator.valid_model?(model)
702
722
  raise(ActiveRecord::RecordInvalid, model) if options[:raise_error]
723
+
703
724
  array_of_attributes[i] = nil
704
725
  failure = model.dup
705
726
  failure.errors.send(:initialize_dup, model.errors)
706
- failed_instances << failure
727
+ failed_instances << (options[:track_validation_failures] ? [i, failure] : failure )
707
728
  end
708
729
  array_of_attributes.compact!
709
730
  end
@@ -723,7 +744,10 @@ class ActiveRecord::Base
723
744
  set_attributes_and_mark_clean(models, return_obj, timestamps, options)
724
745
 
725
746
  # if there are auto-save associations on the models we imported that are new, import them as well
726
- import_associations(models, options.dup) if options[:recursive]
747
+ if options[:recursive]
748
+ options[:on_duplicate_key_update] = on_duplicate_key_update unless on_duplicate_key_update.nil?
749
+ import_associations(models, options.dup.merge(validate: false))
750
+ end
727
751
  end
728
752
 
729
753
  return_obj
@@ -758,27 +782,29 @@ class ActiveRecord::Base
758
782
  def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
759
783
  return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
760
784
 
761
- 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
762
789
  scope_columns, scope_values = scope_attributes.to_a.transpose
763
790
 
764
791
  unless scope_columns.blank?
765
792
  scope_columns.zip(scope_values).each do |name, value|
766
793
  name_as_sym = name.to_sym
767
- next if column_names.include?(name_as_sym)
768
-
769
- is_sti = (name_as_sym == inheritance_column.to_sym && self < base_class)
770
- value = Array(value).first if is_sti
771
-
794
+ next if column_names.include?(name_as_sym) || name_as_sym == inheritance_column.to_sym
772
795
  column_names << name_as_sym
773
796
  array_of_attributes.each { |attrs| attrs << value }
774
797
  end
775
798
  end
776
799
 
777
- columns = column_names.each_with_index.map do |name, i|
778
- column = columns_hash[name.to_s]
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 }
803
+ end
779
804
 
805
+ columns = column_names.each_with_index.map do |name, i|
806
+ column = schema_columns_hash[name.to_s]
780
807
  raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
781
-
782
808
  column
783
809
  end
784
810
 
@@ -794,17 +820,29 @@ class ActiveRecord::Base
794
820
  if supports_import?
795
821
  # generate the sql
796
822
  post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
823
+ import_size = values_sql.size
824
+
825
+ batch_size = options[:batch_size] || import_size
826
+ run_proc = options[:batch_size].to_i.positive? && options[:batch_progress].respond_to?( :call )
827
+ progress_proc = options[:batch_progress]
828
+ current_batch = 0
829
+ batches = (import_size / batch_size.to_f).ceil
797
830
 
798
- batch_size = options[:batch_size] || values_sql.size
799
831
  values_sql.each_slice(batch_size) do |batch_values|
832
+ batch_started_at = Time.now.to_i
833
+
800
834
  # perform the inserts
801
835
  result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
802
836
  batch_values,
803
837
  options,
804
- "#{model_name} Create Many Without Validations Or Callbacks" )
838
+ "#{model_name} Create Many" )
839
+
805
840
  number_inserted += result.num_inserts
806
841
  ids += result.ids
807
842
  results += result.results
843
+ current_batch += 1
844
+
845
+ progress_proc.call(import_size, batches, current_batch, Time.now.to_i - batch_started_at) if run_proc
808
846
  end
809
847
  else
810
848
  transaction(requires_new: true) do
@@ -819,6 +857,14 @@ class ActiveRecord::Base
819
857
 
820
858
  private
821
859
 
860
+ def associated_options(options, association)
861
+ return options unless options.key?(:recursive_on_duplicate_key_update)
862
+
863
+ options.merge(
864
+ on_duplicate_key_update: options[:recursive_on_duplicate_key_update][association]
865
+ )
866
+ end
867
+
822
868
  def set_attributes_and_mark_clean(models, import_result, timestamps, options)
823
869
  return if models.nil?
824
870
  models -= import_result.failed_instances
@@ -830,22 +876,46 @@ class ActiveRecord::Base
830
876
  model.id = id
831
877
 
832
878
  timestamps.each do |attr, value|
833
- model.send(attr + "=", value)
879
+ model.send("#{attr}=", value) if model.send(attr).nil?
834
880
  end
835
881
  end
836
882
  end
837
883
 
838
- if models.size == import_result.results.size
839
- columns = Array(options[:returning])
840
- single_column = "#{columns.first}=" if columns.size == 1
841
- import_result.results.each_with_index do |result, index|
884
+ deserialize_value = lambda do |column, value|
885
+ column = schema_columns_hash[column]
886
+ return value unless column
887
+ if respond_to?(:type_caster)
888
+ type = type_for_attribute(column.name)
889
+ type.deserialize(value)
890
+ elsif column.respond_to?(:type_cast_from_database)
891
+ column.type_cast_from_database(value)
892
+ else
893
+ value
894
+ end
895
+ end
896
+
897
+ set_value = lambda do |model, column, value|
898
+ val = deserialize_value.call(column, value)
899
+ if model.attribute_names.include?(column)
900
+ model.send("#{column}=", val)
901
+ else
902
+ attributes = attributes_builder.build_from_database(model.attributes.merge(column => val))
903
+ model.instance_variable_set(:@attributes, attributes)
904
+ end
905
+ end
906
+
907
+ columns = Array(options[:returning_columns])
908
+ results = Array(import_result.results)
909
+ if models.size == results.size
910
+ single_column = columns.first if columns.size == 1
911
+ results.each_with_index do |result, index|
842
912
  model = models[index]
843
913
 
844
914
  if single_column
845
- model.send(single_column, result)
915
+ set_value.call(model, single_column, result)
846
916
  else
847
917
  columns.each_with_index do |column, col_index|
848
- model.send("#{column}=", result[col_index])
918
+ set_value.call(model, column, result[col_index])
849
919
  end
850
920
  end
851
921
  end
@@ -866,16 +936,22 @@ class ActiveRecord::Base
866
936
 
867
937
  # Sync belongs_to association ids with foreign key field
868
938
  def load_association_ids(model)
939
+ changed_columns = model.changed
869
940
  association_reflections = model.class.reflect_on_all_associations(:belongs_to)
870
941
  association_reflections.each do |association_reflection|
871
- column_name = association_reflection.foreign_key
872
942
  next if association_reflection.options[:polymorphic]
873
- association = model.association(association_reflection.name)
874
- association = association.target
875
- next if association.blank? || model.public_send(column_name).present?
876
943
 
877
- association_primary_key = association_reflection.association_primary_key
878
- model.public_send("#{column_name}=", association.send(association_primary_key))
944
+ column_names = Array(association_reflection.foreign_key).map(&:to_s)
945
+ column_names.each_with_index do |column_name, column_index|
946
+ next if changed_columns.include?(column_name)
947
+
948
+ association = model.association(association_reflection.name)
949
+ association = association.target
950
+ next if association.blank? || model.public_send(column_name).present?
951
+
952
+ association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
953
+ model.public_send("#{column_name}=", association.send(association_primary_key))
954
+ end
879
955
  end
880
956
  end
881
957
 
@@ -888,17 +964,30 @@ class ActiveRecord::Base
888
964
  associated_objects_by_class = {}
889
965
  models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
890
966
 
891
- # :on_duplicate_key_update and :returning not supported for associations
892
- options.delete(:on_duplicate_key_update)
967
+ # :on_duplicate_key_update only supported for all fields
968
+ options.delete(:on_duplicate_key_update) unless options[:on_duplicate_key_update] == :all
969
+ # :returning not supported for associations
893
970
  options.delete(:returning)
894
971
 
895
972
  associated_objects_by_class.each_value do |associations|
896
- associations.each_value do |associated_records|
897
- associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
973
+ associations.each do |association, associated_records|
974
+ next if associated_records.empty?
975
+
976
+ associated_class = associated_records.first.class
977
+ associated_class.bulk_import(associated_records,
978
+ associated_options(options, association))
898
979
  end
899
980
  end
900
981
  end
901
982
 
983
+ def schema_columns_hash
984
+ if respond_to?(:ignored_columns) && ignored_columns.any?
985
+ connection.schema_cache.columns_hash(table_name)
986
+ else
987
+ columns_hash
988
+ end
989
+ end
990
+
902
991
  # We are eventually going to call Class.import <objects> so we build up a hash
903
992
  # of class => objects to import.
904
993
  def find_associated_objects_for_import(associated_objects_by_class, model)
@@ -919,10 +1008,18 @@ class ActiveRecord::Base
919
1008
 
920
1009
  changed_objects = association.select { |a| a.new_record? || a.changed? }
921
1010
  changed_objects.each do |child|
922
- child.public_send("#{association_reflection.foreign_key}=", model.id)
1011
+ Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
1012
+ child.public_send("#{column}=", Array(model.id)[index])
1013
+ end
1014
+
923
1015
  # For polymorphic associations
1016
+ association_name = if model.class.respond_to?(:polymorphic_name)
1017
+ model.class.polymorphic_name
1018
+ else
1019
+ model.class.base_class
1020
+ end
924
1021
  association_reflection.type.try do |type|
925
- child.public_send("#{type}=", model.class.base_class.name)
1022
+ child.public_send("#{type}=", association_name)
926
1023
  end
927
1024
  end
928
1025
  associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
@@ -949,7 +1046,7 @@ class ActiveRecord::Base
949
1046
  elsif column
950
1047
  if respond_to?(:type_caster) # Rails 5.0 and higher
951
1048
  type = type_for_attribute(column.name)
952
- val = type.type == :boolean ? type.cast(val) : type.serialize(val)
1049
+ val = !type.respond_to?(:subtype) && type.type == :boolean ? type.cast(val) : type.serialize(val)
953
1050
  connection_memo.quote(val)
954
1051
  elsif column.respond_to?(:type_cast_from_user) # Rails 4.2
955
1052
  connection_memo.quote(column.type_cast_from_user(val), column)
@@ -977,13 +1074,18 @@ class ActiveRecord::Base
977
1074
  timestamp_columns[:create] = timestamp_attributes_for_create_in_model
978
1075
  timestamp_columns[:update] = timestamp_attributes_for_update_in_model
979
1076
  else
980
- instance = new
1077
+ instance = allocate
981
1078
  timestamp_columns[:create] = instance.send(:timestamp_attributes_for_create_in_model)
982
1079
  timestamp_columns[:update] = instance.send(:timestamp_attributes_for_update_in_model)
983
1080
  end
984
1081
 
985
1082
  # use tz as set in ActiveRecord::Base
986
- timestamp = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
1083
+ default_timezone = if ActiveRecord.respond_to?(:default_timezone)
1084
+ ActiveRecord.default_timezone
1085
+ else
1086
+ ActiveRecord::Base.default_timezone
1087
+ end
1088
+ timestamp = default_timezone == :utc ? Time.now.utc : Time.now
987
1089
 
988
1090
  [:create, :update].each do |action|
989
1091
  timestamp_columns[action].each do |column|
@@ -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,6 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/array'
4
+
1
5
  module ActiveRecord::Import
2
6
  class ValueSetTooLargeError < StandardError
3
7
  attr_reader :size
8
+
4
9
  def initialize(msg = "Value set exceeds max size", size = 0)
5
10
  @size = size
6
11
  super(msg)
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module Import
3
- VERSION = "1.0.4".freeze
5
+ VERSION = "2.0.0"
4
6
  end
5
7
  end
@@ -1,4 +1,5 @@
1
- # rubocop:disable Style/FileName
1
+ # frozen_string_literal: true
2
+
2
3
  require "active_support/lazy_load_hooks"
3
4
 
4
5
  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"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["ARE_DB"] = "trilogy"
4
+
5
+ if ENV['AR_VERSION'].to_f <= 7.0
6
+ require "activerecord-trilogy-adapter"
7
+ require "trilogy_adapter/connection"
8
+ ActiveRecord::Base.extend TrilogyAdapter::Connection
9
+ end
@@ -8,6 +8,7 @@ common: &common
8
8
  mysql2: &mysql2
9
9
  <<: *common
10
10
  adapter: mysql2
11
+ host: mysql
11
12
 
12
13
  mysql2spatial:
13
14
  <<: *mysql2
@@ -19,6 +20,7 @@ postgresql: &postgresql
19
20
  <<: *common
20
21
  username: postgres
21
22
  adapter: postgresql
23
+ host: postgresql
22
24
  min_messages: warning
23
25
 
24
26
  postresql_makara:
@@ -50,3 +52,8 @@ sqlite3: &sqlite3
50
52
 
51
53
  spatialite:
52
54
  <<: *sqlite3
55
+
56
+ trilogy:
57
+ <<: *common
58
+ adapter: trilogy
59
+ host: mysql
@@ -1,8 +1,9 @@
1
1
  common: &common
2
2
  username: root
3
- password:
3
+ password: root
4
4
  encoding: utf8
5
- host: localhost
5
+ collation: utf8_general_ci
6
+ host: 127.0.0.1
6
7
  database: activerecord_import_test
7
8
 
8
9
  jdbcpostgresql: &postgresql
@@ -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
 
@@ -52,7 +54,7 @@ seamless_database_pool:
52
54
  pool_adapter: mysql2
53
55
  prepared_statements: false
54
56
  master:
55
- host: localhost
57
+ host: 127.0.0.1
56
58
 
57
59
  sqlite:
58
60
  adapter: sqlite
@@ -64,3 +66,7 @@ sqlite3: &sqlite3
64
66
 
65
67
  spatialite:
66
68
  <<: *sqlite3
69
+
70
+ trilogy:
71
+ <<: *common
72
+ adapter: trilogy