activerecord-import 0.25.0 → 0.26.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 26224d6c0d4c23f3edaf1faea68466f18c03fbbc
4
- data.tar.gz: 55107f77211b86e3be77b287d224e488dbb280ac
2
+ SHA256:
3
+ metadata.gz: ecfc0672f1042ae828bb3125c0675364d1af8e1b4aefb82af633d3656a8d5fc6
4
+ data.tar.gz: 8b03926293827932a61342fded8e5400fbe8261e92817ce32c09eda7fa16860f
5
5
  SHA512:
6
- metadata.gz: c0815961332178cf2858b39663f1d9f6ef3d61985cd4cb528cef410714e4bf07c354515b5e06124ad4bb5c7e57e7a179c48d7fb5c5e28886bc9e30c4399e82c9
7
- data.tar.gz: df5e8ccd8a658d4981cd26167e09fa8c5e0425c58e4397c48e6f7721221d1de3cc4c6cdf903e3330f07cf6a74b93214cd11cca977f6d75cf6e8b43409807c23b
6
+ metadata.gz: 874795a3c02d59b9411ab831ba6257c0c4dab7bad2b478e69451b3d5344652f4d2f906f727189170dc8417ae64a5a2f236a61da1b24a8c1112a7e731a087a7ca
7
+ data.tar.gz: 00abfb0263c84d2c687451bd1344ae73935291b8db074388c64220d1d6e5ee9dccc515f62217cc4a61c5912acd1de60d5a57ac7229b3734fa145bfd7936f5b56
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## Changes in 0.26.0
2
+
3
+ ### New Features
4
+
5
+ * Add on_duplicate_key_update for SQLite. Thanks to @jkowens via \#542.
6
+ * Add option to update all fields on_duplicate_key_update. Thanks to @aimerald, @jkowens via \#543.
7
+
8
+ ### Fixes
9
+
10
+ * Handle deeply frozen options hashes. Thanks to @jturkel via \#546.
11
+ * Switch from FactoryGirl to FactoryBot. Thanks to @koic via \#547.
12
+ * Allow import to work with ProxySQL. Thanks to @GregFarrell via \#550.
13
+
1
14
  ## Changes in 0.25.0
2
15
 
3
16
  ### New Features
data/Gemfile CHANGED
@@ -29,7 +29,7 @@ platforms :jruby do
29
29
  end
30
30
 
31
31
  # Support libs
32
- gem "factory_girl", "~> 4.2.0"
32
+ gem "factory_bot"
33
33
  gem "timecop"
34
34
  gem "chronic"
35
35
  gem "mocha", "~> 1.3.0"
data/README.markdown CHANGED
@@ -21,11 +21,53 @@ and then the reviews:
21
21
  That would be about 4M SQL insert statements vs 3, which results in vastly improved performance. In our case, it converted
22
22
  an 18 hour batch process to <2 hrs.
23
23
 
24
- ### More Information : Usage and Examples in Wiki
24
+ ## Table of Contents
25
25
 
26
- For more information on activerecord-import please see its wiki: https://github.com/zdennis/activerecord-import/wiki
26
+ * [Callbacks](#callbacks)
27
+ * [Additional Adapters](#additional-adapters)
28
+ * [Load Path Setup](#load-path-setup)
29
+ * [More Information](#more-information)
30
+
31
+ ### Callbacks
32
+
33
+ ActiveRecord callbacks related to [creating](http://guides.rubyonrails.org/active_record_callbacks.html#creating-an-object), [updating](http://guides.rubyonrails.org/active_record_callbacks.html#updating-an-object), or [destroying](http://guides.rubyonrails.org/active_record_callbacks.html#destroying-an-object) records (other than `before_validation` and `after_validation`) will NOT be called when calling the import method. This is because it is mass importing rows of data and doesn't necessarily have access to in-memory ActiveRecord objects.
34
+
35
+ If you do have a collection of in-memory ActiveRecord objects you can do something like this:
36
+
37
+ ```
38
+ books.each do |book|
39
+ book.run_callbacks(:save) { false }
40
+ book.run_callbacks(:create) { false }
41
+ end
42
+ Book.import(books)
43
+ ```
44
+
45
+ This will run before_create and before_save callbacks on each item. The `false` argument is needed to prevent after_save being run, which wouldn't make sense prior to bulk import. Something to note in this example is that the before_create and before_save callbacks will run before the validation callbacks.
46
+
47
+ If that is an issue, another possible approach is to loop through your models first to do validations and then only run callbacks on and import the valid models.
48
+
49
+ ```
50
+ valid_books = []
51
+ invalid_books = []
52
+
53
+ books.each do |book|
54
+ if book.valid?
55
+ valid_books << book
56
+ else
57
+ invalid_books << book
58
+ end
59
+ end
60
+
61
+ valid_books.each do |book|
62
+ book.run_callbacks(:save) { false }
63
+ book.run_callbacks(:create) { false }
64
+ end
65
+
66
+ Book.import valid_books, validate: false
67
+ ```
68
+
69
+ ### Additional Adapters
27
70
 
28
- ## Additional Adapters
29
71
  Additional adapters can be provided by gems external to activerecord-import by providing an adapter that matches the naming convention setup by activerecord-import (and subsequently activerecord) for dynamically loading adapters. This involves also providing a folder on the load path that follows the activerecord-import naming convention to allow activerecord-import to dynamically load the file.
30
72
 
31
73
  When `ActiveRecord::Import.require_adapter("fake_name")` is called the require will be:
@@ -63,6 +105,12 @@ activerecord-import-fake_name/
63
105
 
64
106
  When rubygems pushes the `lib` folder onto the load path a `require` will now find `activerecord-import/active_record/adapters/fake_name_adapter` as it runs through the lookup process for a ruby file under that path in `$LOAD_PATH`
65
107
 
108
+ ### More Information
109
+
110
+ For more information on activerecord-import please see its wiki: https://github.com/zdennis/activerecord-import/wiki
111
+
112
+ To document new information, please add to the README instead of the wiki. See https://github.com/zdennis/activerecord-import/issues/397 for discussion.
113
+
66
114
  # License
67
115
 
68
116
  This is licensed under the ruby license.
data/Rakefile CHANGED
@@ -32,7 +32,7 @@ ADAPTERS.each do |adapter|
32
32
  namespace :test do
33
33
  desc "Runs #{adapter} database tests."
34
34
  Rake::TestTask.new(adapter) do |t|
35
- # FactoryGirl has an issue with warnings, so turn off, so noisy
35
+ # FactoryBot has an issue with warnings, so turn off, so noisy
36
36
  # t.warning = true
37
37
  t.test_files = FileList["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/active_record/*_test.rb", "test/#{adapter}/**/*_test.rb"]
38
38
  end
@@ -56,7 +56,7 @@ module ActiveRecord::Import::MysqlAdapter
56
56
  # in a single packet
57
57
  def max_allowed_packet # :nodoc:
58
58
  @max_allowed_packet ||= begin
59
- result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
59
+ result = execute( "SHOW VARIABLES like 'max_allowed_packet'" )
60
60
  # original Mysql gem responds to #fetch_row while Mysql2 responds to #first
61
61
  val = result.respond_to?(:fetch_row) ? result.fetch_row[1] : result.first[1]
62
62
  val.to_i
@@ -1,7 +1,9 @@
1
1
  module ActiveRecord::Import::SQLite3Adapter
2
2
  include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
4
 
4
5
  MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
6
+ MIN_VERSION_FOR_UPSERT = "3.24.0".freeze
5
7
  SQLITE_LIMIT_COMPOUND_SELECT = 500
6
8
 
7
9
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
@@ -15,6 +17,10 @@ module ActiveRecord::Import::SQLite3Adapter
15
17
  end
16
18
  end
17
19
 
20
+ def supports_on_duplicate_key_update?(current_version = sqlite_version)
21
+ current_version >= MIN_VERSION_FOR_UPSERT
22
+ end
23
+
18
24
  # +sql+ can be a single string or an array. If it is an array all
19
25
  # elements that are in position >= 1 will be appended to the final SQL.
20
26
  def insert_many( sql, values, _options = {}, *args ) # :nodoc:
@@ -40,16 +46,133 @@ module ActiveRecord::Import::SQLite3Adapter
40
46
  ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
41
47
  end
42
48
 
43
- def pre_sql_statements( options)
49
+ def pre_sql_statements( options )
44
50
  sql = []
45
51
  # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
46
- if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:recursive]
52
+ if !supports_on_duplicate_key_update? && (options[:ignore] || options[:on_duplicate_key_ignore])
47
53
  sql << "OR IGNORE"
48
54
  end
49
55
  sql + super
50
56
  end
51
57
 
58
+ def post_sql_statements( table_name, options ) # :nodoc:
59
+ sql = []
60
+
61
+ if supports_on_duplicate_key_update?
62
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
63
+ if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update]
64
+ sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
65
+ end
66
+ end
67
+
68
+ sql + super
69
+ end
70
+
52
71
  def next_value_for_sequence(sequence_name)
53
72
  %{nextval('#{sequence_name}')}
54
73
  end
74
+
75
+ # Add a column to be updated on duplicate key update
76
+ def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
77
+ arg = options[:on_duplicate_key_update]
78
+ if arg.is_a?( Hash )
79
+ columns = arg.fetch( :columns ) { arg[:columns] = [] }
80
+ case columns
81
+ when Array then columns << column.to_sym unless columns.include?( column.to_sym )
82
+ when Hash then columns[column.to_sym] = column.to_sym
83
+ end
84
+ elsif arg.is_a?( Array )
85
+ arg << column.to_sym unless arg.include?( column.to_sym )
86
+ end
87
+ end
88
+
89
+ # Returns a generated ON CONFLICT DO NOTHING statement given the passed
90
+ # in +args+.
91
+ def sql_for_on_duplicate_key_ignore( *args ) # :nodoc:
92
+ arg = args.first
93
+ conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
94
+ " ON CONFLICT #{conflict_target}DO NOTHING"
95
+ end
96
+
97
+ # Returns a generated ON CONFLICT DO UPDATE statement given the passed
98
+ # in +args+.
99
+ def sql_for_on_duplicate_key_update( _table_name, *args ) # :nodoc:
100
+ arg, primary_key, locking_column = args
101
+ arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
102
+ return unless arg.is_a?( Hash )
103
+
104
+ sql = ' ON CONFLICT '
105
+ conflict_target = sql_for_conflict_target( arg )
106
+
107
+ columns = arg.fetch( :columns, [] )
108
+ condition = arg[:condition]
109
+ if columns.respond_to?( :empty? ) && columns.empty?
110
+ return sql << "#{conflict_target}DO NOTHING"
111
+ end
112
+
113
+ conflict_target ||= sql_for_default_conflict_target( primary_key )
114
+ unless conflict_target
115
+ raise ArgumentError, 'Expected :conflict_target to be specified'
116
+ end
117
+
118
+ sql << "#{conflict_target}DO UPDATE SET "
119
+ if columns.is_a?( Array )
120
+ sql << sql_for_on_duplicate_key_update_as_array( locking_column, columns )
121
+ elsif columns.is_a?( Hash )
122
+ sql << sql_for_on_duplicate_key_update_as_hash( locking_column, columns )
123
+ elsif columns.is_a?( String )
124
+ sql << columns
125
+ else
126
+ raise ArgumentError, 'Expected :columns to be an Array or Hash'
127
+ end
128
+
129
+ sql << " WHERE #{condition}" if condition.present?
130
+
131
+ sql
132
+ end
133
+
134
+ def sql_for_on_duplicate_key_update_as_array( locking_column, arr ) # :nodoc:
135
+ results = arr.map do |column|
136
+ qc = quote_column_name( column )
137
+ "#{qc}=EXCLUDED.#{qc}"
138
+ end
139
+ increment_locking_column!(results, locking_column)
140
+ results.join( ',' )
141
+ end
142
+
143
+ def sql_for_on_duplicate_key_update_as_hash( locking_column, hsh ) # :nodoc:
144
+ results = hsh.map do |column1, column2|
145
+ qc1 = quote_column_name( column1 )
146
+ qc2 = quote_column_name( column2 )
147
+ "#{qc1}=EXCLUDED.#{qc2}"
148
+ end
149
+ increment_locking_column!(results, locking_column)
150
+ results.join( ',' )
151
+ end
152
+
153
+ def sql_for_conflict_target( args = {} )
154
+ conflict_target = args[:conflict_target]
155
+ index_predicate = args[:index_predicate]
156
+ if conflict_target.present?
157
+ '(' << Array( conflict_target ).reject( &:blank? ).join( ', ' ) << ') '.tap do |sql|
158
+ sql << "WHERE #{index_predicate} " if index_predicate
159
+ end
160
+ end
161
+ end
162
+
163
+ def sql_for_default_conflict_target( primary_key )
164
+ conflict_target = Array(primary_key).join(', ')
165
+ "(#{conflict_target}) " if conflict_target.present?
166
+ end
167
+
168
+ # Return true if the statement is a duplicate key record error
169
+ def duplicate_key_update_error?(exception) # :nodoc:
170
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
171
+ end
172
+
173
+ def increment_locking_column!(results, locking_column)
174
+ if locking_column.present?
175
+ results << "\"#{locking_column}\"=EXCLUDED.\"#{locking_column}\"+1"
176
+ end
177
+ end
55
178
  end
@@ -298,8 +298,8 @@ class ActiveRecord::Base
298
298
  # recursive import. For database adapters that normally support
299
299
  # setting primary keys on imported objects, this option prevents
300
300
  # that from occurring.
301
- # * +on_duplicate_key_update+ - an Array or Hash, tells import to
302
- # use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
301
+ # * +on_duplicate_key_update+ - :all, an Array, or Hash, tells import to
302
+ # use MySQL's ON DUPLICATE KEY UPDATE or Postgres/SQLite ON CONFLICT
303
303
  # DO UPDATE ability. See On Duplicate Key Update below.
304
304
  # * +synchronize+ - an array of ActiveRecord instances for the model
305
305
  # that you are currently importing data into. This synchronizes
@@ -358,7 +358,15 @@ class ActiveRecord::Base
358
358
  #
359
359
  # == On Duplicate Key Update (MySQL)
360
360
  #
361
- # The :on_duplicate_key_update option can be either an Array or a Hash.
361
+ # The :on_duplicate_key_update option can be either :all, an Array, or a Hash.
362
+ #
363
+ # ==== Using :all
364
+ #
365
+ # The :on_duplicate_key_update option can be set to :all. All columns
366
+ # other than the primary key are updated. If a list of column names is
367
+ # supplied, only those columns will be updated. Below is an example:
368
+ #
369
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
362
370
  #
363
371
  # ==== Using an Array
364
372
  #
@@ -377,11 +385,19 @@ class ActiveRecord::Base
377
385
  #
378
386
  # BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
379
387
  #
380
- # == On Duplicate Key Update (Postgres 9.5+)
388
+ # == On Duplicate Key Update (Postgres 9.5+ and SQLite 3.24+)
381
389
  #
382
- # The :on_duplicate_key_update option can be an Array or a Hash with up to
390
+ # The :on_duplicate_key_update option can be :all, an Array, or a Hash with up to
383
391
  # three attributes, :conflict_target (and optionally :index_predicate) or
384
- # :constraint_name, and :columns.
392
+ # :constraint_name (Postgres), and :columns.
393
+ #
394
+ # ==== Using :all
395
+ #
396
+ # The :on_duplicate_key_update option can be set to :all. All columns
397
+ # other than the primary key are updated. If a list of column names is
398
+ # supplied, only those columns will be updated. Below is an example:
399
+ #
400
+ # BlogPost.import columns, values, on_duplicate_key_update: :all
385
401
  #
386
402
  # ==== Using an Array
387
403
  #
@@ -439,7 +455,15 @@ class ActiveRecord::Base
439
455
  #
440
456
  # ===== :columns
441
457
  #
442
- # The :columns attribute can be either an Array or a Hash.
458
+ # The :columns attribute can be either :all, an Array, or a Hash.
459
+ #
460
+ # ===== Using :all
461
+ #
462
+ # The :columns attribute can be :all. All columns other than the primary key will be updated.
463
+ # If a list of column names is supplied, only those columns will be updated.
464
+ # Below is an example:
465
+ #
466
+ # BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: :all }
443
467
  #
444
468
  # ===== Using an Array
445
469
  #
@@ -495,16 +519,6 @@ class ActiveRecord::Base
495
519
  options[:primary_key] = primary_key
496
520
  options[:locking_column] = locking_column if attribute_names.include?(locking_column)
497
521
 
498
- # Don't modify incoming arguments
499
- on_duplicate_key_update = options[:on_duplicate_key_update]
500
- if on_duplicate_key_update && on_duplicate_key_update.duplicable?
501
- options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
502
- on_duplicate_key_update.each { |k, v| on_duplicate_key_update[k] = v.dup if v.duplicable? }
503
- else
504
- on_duplicate_key_update.dup
505
- end
506
- end
507
-
508
522
  is_validating = options[:validate_with_context].present? ? true : options[:validate]
509
523
  validator = ActiveRecord::Import::Validator.new(options)
510
524
 
@@ -522,8 +536,12 @@ class ActiveRecord::Base
522
536
  end
523
537
  end
524
538
 
525
- if models.first.id.nil? && column_names.include?(primary_key) && columns_hash[primary_key].type == :uuid
526
- column_names.delete(primary_key)
539
+ if models.first.id.nil?
540
+ Array(primary_key).each do |c|
541
+ if column_names.include?(c) && columns_hash[c].type == :uuid
542
+ column_names.delete(c)
543
+ end
544
+ end
527
545
  end
528
546
 
529
547
  default_values = column_defaults
@@ -611,6 +629,29 @@ class ActiveRecord::Base
611
629
  array_of_attributes.each { |a| a.concat(new_fields) }
612
630
  end
613
631
 
632
+ # Don't modify incoming arguments
633
+ on_duplicate_key_update = options[:on_duplicate_key_update]
634
+ if on_duplicate_key_update
635
+ updatable_columns = symbolized_column_names.reject { |c| symbolized_primary_key.include? c }
636
+ options[:on_duplicate_key_update] = if on_duplicate_key_update.is_a?(Hash)
637
+ on_duplicate_key_update.each_with_object({}) do |(k, v), duped_options|
638
+ duped_options[k] = if k == :columns && v == :all
639
+ updatable_columns
640
+ elsif v.duplicable?
641
+ v.dup
642
+ else
643
+ v
644
+ end
645
+ end
646
+ elsif on_duplicate_key_update == :all
647
+ updatable_columns
648
+ elsif on_duplicate_key_update.duplicable?
649
+ on_duplicate_key_update.dup
650
+ else
651
+ on_duplicate_key_update
652
+ end
653
+ end
654
+
614
655
  timestamps = {}
615
656
 
616
657
  # record timestamps unless disabled in ActiveRecord::Base
@@ -646,7 +687,7 @@ class ActiveRecord::Base
646
687
  end
647
688
 
648
689
  if options[:synchronize]
649
- sync_keys = options[:synchronize_keys] || [primary_key]
690
+ sync_keys = options[:synchronize_keys] || Array(primary_key)
650
691
  synchronize( options[:synchronize], sync_keys)
651
692
  end
652
693
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
@@ -871,7 +912,7 @@ class ActiveRecord::Base
871
912
  column = columns[j]
872
913
 
873
914
  # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
874
- if val.nil? && column.name == primary_key && !sequence_name.blank?
915
+ if val.nil? && Array(primary_key).first == column.name && !sequence_name.blank?
875
916
  connection_memo.next_value_for_sequence(sequence_name)
876
917
  elsif val.respond_to?(:to_sql)
877
918
  "(#{val.to_sql})"
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Import
3
- VERSION = "0.25.0".freeze
3
+ VERSION = "0.26.0".freeze
4
4
  end
5
5
  end
data/test/import_test.rb CHANGED
@@ -567,7 +567,7 @@ describe "#import" do
567
567
 
568
568
  context "importing through an association scope" do
569
569
  { has_many: :chapters, polymorphic: :discounts }.each do |association_type, association|
570
- book = FactoryGirl.create :book
570
+ book = FactoryBot.create :book
571
571
  scope = book.public_send association
572
572
  klass = { chapters: Chapter, discounts: Discount }[association]
573
573
  column = { chapters: :title, discounts: :amount }[association]
@@ -609,7 +609,7 @@ describe "#import" do
609
609
 
610
610
  context "importing model with polymorphic belongs_to" do
611
611
  it "works without error" do
612
- book = FactoryGirl.create :book
612
+ book = FactoryBot.create :book
613
613
  discount = Discount.new(discountable: book)
614
614
 
615
615
  Discount.import([discount])
@@ -0,0 +1,13 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :alarms, force: true do |t|
3
+ t.column :device_id, :integer, null: false
4
+ t.column :alarm_type, :integer, null: false
5
+ t.column :status, :integer, null: false
6
+ t.column :metadata, :text
7
+ t.column :secret_key, :binary
8
+ t.datetime :created_at
9
+ t.datetime :updated_at
10
+ end
11
+
12
+ add_index :alarms, [:device_id, :alarm_type], unique: true, where: 'status <> 0'
13
+ end
@@ -1,4 +1,4 @@
1
- FactoryGirl.define do
1
+ FactoryBot.define do
2
2
  sequence(:book_title) { |n| "Book #{n}" }
3
3
  sequence(:chapter_title) { |n| "Chapter #{n}" }
4
4
  sequence(:end_note) { |n| "Endnote #{n}" }
@@ -9,7 +9,7 @@ FactoryGirl.define do
9
9
 
10
10
  factory :invalid_topic, class: "Topic" do
11
11
  sequence(:title) { |n| "Title #{n}" }
12
- author_name nil
12
+ author_name { nil }
13
13
  end
14
14
 
15
15
  factory :topic do
@@ -27,7 +27,7 @@ FactoryGirl.define do
27
27
 
28
28
  trait :with_rule do
29
29
  after(:build) do |question|
30
- question.build_rule(FactoryGirl.attributes_for(:rule))
30
+ question.build_rule(FactoryBot.attributes_for(:rule))
31
31
  end
32
32
  end
33
33
  end
@@ -40,21 +40,21 @@ FactoryGirl.define do
40
40
  factory :topic_with_book, parent: :topic do
41
41
  after(:build) do |topic|
42
42
  2.times do
43
- book = topic.books.build(title: FactoryGirl.generate(:book_title), author_name: 'Stephen King')
43
+ book = topic.books.build(title: FactoryBot.generate(:book_title), author_name: 'Stephen King')
44
44
  3.times do
45
- book.chapters.build(title: FactoryGirl.generate(:chapter_title))
45
+ book.chapters.build(title: FactoryBot.generate(:chapter_title))
46
46
  end
47
47
 
48
48
  4.times do
49
- book.end_notes.build(note: FactoryGirl.generate(:end_note))
49
+ book.end_notes.build(note: FactoryBot.generate(:end_note))
50
50
  end
51
51
  end
52
52
  end
53
53
  end
54
54
 
55
55
  factory :book do
56
- title 'Tortilla Flat'
57
- author_name 'John Steinbeck'
56
+ title { 'Tortilla Flat' }
57
+ author_name { 'John Steinbeck' }
58
58
  end
59
59
 
60
60
  factory :car do
@@ -2,28 +2,28 @@ class ActiveSupport::TestCase
2
2
  def Build(*args) # rubocop:disable Style/MethodName
3
3
  n = args.shift if args.first.is_a?(Numeric)
4
4
  factory = args.shift
5
- factory_girl_args = args.shift || {}
5
+ factory_bot_args = args.shift || {}
6
6
 
7
7
  if n
8
8
  [].tap do |collection|
9
- n.times.each { collection << FactoryGirl.build(factory.to_s.singularize.to_sym, factory_girl_args) }
9
+ n.times.each { collection << FactoryBot.build(factory.to_s.singularize.to_sym, factory_bot_args) }
10
10
  end
11
11
  else
12
- FactoryGirl.build(factory.to_s.singularize.to_sym, factory_girl_args)
12
+ FactoryBot.build(factory.to_s.singularize.to_sym, factory_bot_args)
13
13
  end
14
14
  end
15
15
 
16
16
  def Generate(*args) # rubocop:disable Style/MethodName
17
17
  n = args.shift if args.first.is_a?(Numeric)
18
18
  factory = args.shift
19
- factory_girl_args = args.shift || {}
19
+ factory_bot_args = args.shift || {}
20
20
 
21
21
  if n
22
22
  [].tap do |collection|
23
- n.times.each { collection << FactoryGirl.create(factory.to_s.singularize.to_sym, factory_girl_args) }
23
+ n.times.each { collection << FactoryBot.create(factory.to_s.singularize.to_sym, factory_bot_args) }
24
24
  end
25
25
  else
26
- FactoryGirl.create(factory.to_s.singularize.to_sym, factory_girl_args)
26
+ FactoryBot.create(factory.to_s.singularize.to_sym, factory_bot_args)
27
27
  end
28
28
  end
29
29
  end
@@ -295,6 +295,30 @@ def should_support_postgresql_upsert_functionality
295
295
  end
296
296
 
297
297
  context "using a hash" do
298
+ context "with :columns :all" do
299
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
300
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
301
+
302
+ macro(:perform_import) do |*opts|
303
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
304
+ end
305
+
306
+ setup do
307
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
308
+ Topic.import columns + ['replies_count'], values, validate: false
309
+ end
310
+
311
+ it "should update all specified columns" do
312
+ perform_import
313
+ updated_topic = Topic.find(99)
314
+ assert_equal 'Book - 2nd Edition', updated_topic.title
315
+ assert_equal 'Jane Doe', updated_topic.author_name
316
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
317
+ assert_equal 57, updated_topic.parent_id
318
+ assert_equal 3, updated_topic.replies_count
319
+ end
320
+ end
321
+
298
322
  context "with :columns a hash" do
299
323
  let(:columns) { %w( id title author_name author_email_address parent_id ) }
300
324
  let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
@@ -312,7 +336,7 @@ def should_support_postgresql_upsert_functionality
312
336
  it "should not modify the passed in :on_duplicate_key_update columns array" do
313
337
  assert_nothing_raised do
314
338
  columns = %w(title author_name).freeze
315
- Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: { columns: columns }
339
+ Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
316
340
  end
317
341
  end
318
342
 
@@ -213,6 +213,30 @@ def should_support_basic_on_duplicate_key_update
213
213
  end
214
214
 
215
215
  context "with :on_duplicate_key_update" do
216
+ describe 'using :all' do
217
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
218
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
219
+
220
+ macro(:perform_import) do |*opts|
221
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: :all, validate: false)
222
+ end
223
+
224
+ setup do
225
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
226
+ Topic.import columns + ['replies_count'], values, validate: false
227
+ end
228
+
229
+ it 'updates all specified columns' do
230
+ perform_import
231
+ updated_topic = Topic.find(99)
232
+ assert_equal 'Book - 2nd Edition', updated_topic.title
233
+ assert_equal 'Jane Doe', updated_topic.author_name
234
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
235
+ assert_equal 57, updated_topic.parent_id
236
+ assert_equal 3, updated_topic.replies_count
237
+ end
238
+ end
239
+
216
240
  describe "argument safety" do
217
241
  it "should not modify the passed in :on_duplicate_key_update array" do
218
242
  assert_nothing_raised do
@@ -11,7 +11,7 @@ def should_support_recursive_import
11
11
  let(:num_chapters) { 18 }
12
12
  let(:num_endnotes) { 24 }
13
13
 
14
- let(:new_question_with_rule) { FactoryGirl.build :question, :with_rule }
14
+ let(:new_question_with_rule) { FactoryBot.build :question, :with_rule }
15
15
 
16
16
  it 'imports top level' do
17
17
  assert_difference "Topic.count", +num_topics do
@@ -1,6 +1,8 @@
1
1
  # encoding: UTF-8
2
2
  def should_support_sqlite3_import_functionality
3
- should_support_on_duplicate_key_ignore
3
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
4
+ should_support_sqlite_upsert_functionality
5
+ end
4
6
 
5
7
  describe "#supports_imports?" do
6
8
  context "and SQLite is 3.7.11 or higher" do
@@ -49,18 +51,193 @@ def should_support_sqlite3_import_functionality
49
51
  assert_equal 2500, Topic.count, "Failed to insert all records. Make sure you have a supported version of SQLite3 (3.7.11 or higher) installed"
50
52
  end
51
53
  end
54
+ end
55
+ end
56
+
57
+ def should_support_sqlite_upsert_functionality
58
+ should_support_basic_on_duplicate_key_update
59
+ should_support_on_duplicate_key_ignore
60
+
61
+ describe "#import" do
62
+ extend ActiveSupport::TestCase::ImportAssertions
63
+
64
+ macro(:perform_import) { raise "supply your own #perform_import in a context below" }
65
+ macro(:updated_topic) { Topic.find(@topic.id) }
66
+
67
+ context "with :on_duplicate_key_ignore and validation checks turned off" do
68
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
69
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
70
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
71
+
72
+ setup do
73
+ Topic.import columns, values, validate: false
74
+ end
75
+
76
+ it "should not update any records" do
77
+ result = Topic.import columns, updated_values, on_duplicate_key_ignore: true, validate: false
78
+ assert_equal [], result.ids
79
+ end
80
+ end
81
+
82
+ context "with :on_duplicate_key_update and validation checks turned off" do
83
+ asssertion_group(:should_support_on_duplicate_key_update) do
84
+ should_not_update_fields_not_mentioned
85
+ should_update_foreign_keys
86
+ should_not_update_created_at_on_timestamp_columns
87
+ should_update_updated_at_on_timestamp_columns
88
+ end
89
+
90
+ context "using a hash" do
91
+ context "with :columns a hash" do
92
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
93
+ let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
94
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Author Should Not Change", "johndoe@example.com", 57]] }
95
+
96
+ macro(:perform_import) do |*opts|
97
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: update_columns }, validate: false)
98
+ end
99
+
100
+ setup do
101
+ Topic.import columns, values, validate: false
102
+ @topic = Topic.find 99
103
+ end
104
+
105
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
106
+ assert_nothing_raised do
107
+ columns = %w(title author_name).freeze
108
+ Topic.import columns, [%w(foo, bar)], on_duplicate_key_update: { columns: columns }
109
+ end
110
+ end
111
+
112
+ context "using string hash map" do
113
+ let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
114
+ should_support_on_duplicate_key_update
115
+ should_update_fields_mentioned
116
+ end
117
+
118
+ context "using string hash map, but specifying column mismatches" do
119
+ let(:update_columns) { { "title" => "author_email_address", "author_email_address" => "title", "parent_id" => "parent_id" } }
120
+ should_support_on_duplicate_key_update
121
+ should_update_fields_mentioned_with_hash_mappings
122
+ end
123
+
124
+ context "using symbol hash map" do
125
+ let(:update_columns) { { title: :title, author_email_address: :author_email_address, parent_id: :parent_id } }
126
+ should_support_on_duplicate_key_update
127
+ should_update_fields_mentioned
128
+ end
129
+
130
+ context "using symbol hash map, but specifying column mismatches" do
131
+ let(:update_columns) { { title: :author_email_address, author_email_address: :title, parent_id: :parent_id } }
132
+ should_support_on_duplicate_key_update
133
+ should_update_fields_mentioned_with_hash_mappings
134
+ end
135
+ end
136
+
137
+ context 'with :index_predicate' do
138
+ let(:columns) { %w( id device_id alarm_type status metadata ) }
139
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
140
+ let(:updated_values) { [[99, 17, 1, 2, 'bar']] }
141
+
142
+ macro(:perform_import) do |*opts|
143
+ Alarm.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: [:device_id, :alarm_type], index_predicate: 'status <> 0', columns: [:status] }, validate: false)
144
+ end
145
+
146
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
147
+
148
+ setup do
149
+ Alarm.import columns, values, validate: false
150
+ @alarm = Alarm.find 99
151
+ end
152
+
153
+ context 'supports on duplicate key update for partial indexes' do
154
+ it 'should not update created_at timestamp columns' do
155
+ Timecop.freeze Chronic.parse("5 minutes from now") do
156
+ perform_import
157
+ assert_in_delta @alarm.created_at.to_i, updated_alarm.created_at.to_i, 1
158
+ end
159
+ end
160
+
161
+ it 'should update updated_at timestamp columns' do
162
+ time = Chronic.parse("5 minutes from now")
163
+ Timecop.freeze time do
164
+ perform_import
165
+ assert_in_delta time.to_i, updated_alarm.updated_at.to_i, 1
166
+ end
167
+ end
168
+
169
+ it 'should not update fields not mentioned' do
170
+ perform_import
171
+ assert_equal 'foo', updated_alarm.metadata
172
+ end
173
+
174
+ it 'should update fields mentioned with hash mappings' do
175
+ perform_import
176
+ assert_equal 2, updated_alarm.status
177
+ end
178
+ end
179
+ end
180
+
181
+ context 'with :condition' do
182
+ let(:columns) { %w( id device_id alarm_type status metadata) }
183
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
184
+ let(:updated_values) { [[99, 17, 1, 1, 'bar']] }
185
+
186
+ macro(:perform_import) do |*opts|
187
+ Alarm.import(
188
+ columns,
189
+ updated_values,
190
+ opts.extract_options!.merge(
191
+ on_duplicate_key_update: {
192
+ conflict_target: [:id],
193
+ condition: "alarms.metadata NOT LIKE '%foo%'",
194
+ columns: [:metadata]
195
+ },
196
+ validate: false
197
+ )
198
+ )
199
+ end
200
+
201
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
202
+
203
+ setup do
204
+ Alarm.import columns, values, validate: false
205
+ @alarm = Alarm.find 99
206
+ end
207
+
208
+ it 'should not update fields not matched' do
209
+ perform_import
210
+ assert_equal 'foo', updated_alarm.metadata
211
+ end
212
+ end
213
+
214
+ context "with no :conflict_target" do
215
+ context "with no primary key" do
216
+ it "raises ArgumentError" do
217
+ error = assert_raises ArgumentError do
218
+ Rule.import Build(3, :rules), on_duplicate_key_update: [:condition_text], validate: false
219
+ end
220
+ assert_match(/Expected :conflict_target to be specified/, error.message)
221
+ end
222
+ end
223
+ end
224
+
225
+ context "with no :columns" do
226
+ let(:columns) { %w( id title author_name author_email_address ) }
227
+ let(:values) { [[100, "Book", "John Doe", "john@doe.com"]] }
228
+ let(:updated_values) { [[100, "Title Should Not Change", "Author Should Not Change", "john@nogo.com"]] }
52
229
 
53
- context "with :on_duplicate_key_update" do
54
- let(:topics) { Build(1, :topics) }
230
+ macro(:perform_import) do |*opts|
231
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id }, validate: false)
232
+ end
55
233
 
56
- it "should log a warning message" do
57
- log = StringIO.new
58
- logger = Logger.new(log)
59
- logger.level = Logger::WARN
60
- ActiveRecord::Base.connection.stubs(:logger).returns(logger)
234
+ setup do
235
+ Topic.import columns, values, validate: false
236
+ @topic = Topic.find 100
237
+ end
61
238
 
62
- Topic.import topics, on_duplicate_key_update: true
63
- assert_match(/Ignoring on_duplicate_key_update/, log.string)
239
+ should_update_updated_at_on_timestamp_columns
240
+ end
64
241
  end
65
242
  end
66
243
  end
data/test/test_helper.rb CHANGED
@@ -58,7 +58,7 @@ ActiveSupport::Notifications.subscribe(/active_record.sql/) do |_, _, _, _, hsh|
58
58
  ActiveRecord::Base.logger.info hsh[:sql]
59
59
  end
60
60
 
61
- require "factory_girl"
61
+ require "factory_bot"
62
62
  Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each { |file| require file }
63
63
 
64
64
  # Load base/generic schema
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-import
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.0
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Dennis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-10 00:00:00.000000000 Z
11
+ date: 2018-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -146,6 +146,7 @@ files:
146
146
  - test/schema/mysql2_schema.rb
147
147
  - test/schema/postgis_schema.rb
148
148
  - test/schema/postgresql_schema.rb
149
+ - test/schema/sqlite3_schema.rb
149
150
  - test/schema/version.rb
150
151
  - test/sqlite3/import_test.rb
151
152
  - test/support/active_support/test_case_extensions.rb
@@ -183,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
184
  version: '0'
184
185
  requirements: []
185
186
  rubyforge_project:
186
- rubygems_version: 2.6.13
187
+ rubygems_version: 2.7.7
187
188
  signing_key:
188
189
  specification_version: 4
189
190
  summary: Bulk insert extension for ActiveRecord
@@ -236,6 +237,7 @@ test_files:
236
237
  - test/schema/mysql2_schema.rb
237
238
  - test/schema/postgis_schema.rb
238
239
  - test/schema/postgresql_schema.rb
240
+ - test/schema/sqlite3_schema.rb
239
241
  - test/schema/version.rb
240
242
  - test/sqlite3/import_test.rb
241
243
  - test/support/active_support/test_case_extensions.rb