activerecord-import 1.0.4 → 2.0.0

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