activerecord-import 0.17.2 → 1.1.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 (52) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +40 -23
  4. data/CHANGELOG.md +315 -1
  5. data/Gemfile +23 -13
  6. data/LICENSE +21 -56
  7. data/README.markdown +564 -33
  8. data/Rakefile +2 -1
  9. data/activerecord-import.gemspec +3 -3
  10. data/benchmarks/lib/cli_parser.rb +2 -1
  11. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +0 -0
  12. data/gemfiles/5.1.gemfile +2 -0
  13. data/gemfiles/5.2.gemfile +2 -0
  14. data/gemfiles/6.0.gemfile +2 -0
  15. data/gemfiles/6.1.gemfile +1 -0
  16. data/lib/activerecord-import.rb +2 -15
  17. data/lib/activerecord-import/adapters/abstract_adapter.rb +9 -3
  18. data/lib/activerecord-import/adapters/mysql_adapter.rb +17 -11
  19. data/lib/activerecord-import/adapters/postgresql_adapter.rb +68 -20
  20. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
  21. data/lib/activerecord-import/base.rb +12 -7
  22. data/lib/activerecord-import/import.rb +514 -166
  23. data/lib/activerecord-import/synchronize.rb +2 -2
  24. data/lib/activerecord-import/value_sets_parser.rb +16 -0
  25. data/lib/activerecord-import/version.rb +1 -1
  26. data/test/adapters/makara_postgis.rb +1 -0
  27. data/test/import_test.rb +274 -23
  28. data/test/makara_postgis/import_test.rb +8 -0
  29. data/test/models/account.rb +3 -0
  30. data/test/models/animal.rb +6 -0
  31. data/test/models/bike_maker.rb +7 -0
  32. data/test/models/tag.rb +1 -1
  33. data/test/models/topic.rb +14 -0
  34. data/test/models/user.rb +3 -0
  35. data/test/models/user_token.rb +4 -0
  36. data/test/schema/generic_schema.rb +30 -8
  37. data/test/schema/mysql2_schema.rb +19 -0
  38. data/test/schema/postgresql_schema.rb +18 -0
  39. data/test/schema/sqlite3_schema.rb +13 -0
  40. data/test/support/factories.rb +9 -8
  41. data/test/support/generate.rb +6 -6
  42. data/test/support/mysql/import_examples.rb +14 -2
  43. data/test/support/postgresql/import_examples.rb +220 -1
  44. data/test/support/shared_examples/on_duplicate_key_ignore.rb +15 -9
  45. data/test/support/shared_examples/on_duplicate_key_update.rb +271 -8
  46. data/test/support/shared_examples/recursive_import.rb +91 -21
  47. data/test/support/sqlite3/import_examples.rb +189 -25
  48. data/test/synchronize_test.rb +8 -0
  49. data/test/test_helper.rb +24 -3
  50. data/test/value_sets_bytes_parser_test.rb +13 -2
  51. metadata +32 -13
  52. data/test/schema/mysql_schema.rb +0 -16
@@ -0,0 +1,8 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
+
4
+ should_support_postgresql_import_functionality
5
+
6
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
7
+ should_support_postgresql_upsert_functionality
8
+ end
@@ -0,0 +1,3 @@
1
+ class Account < ActiveRecord::Base
2
+ self.locking_column = :lock
3
+ end
@@ -0,0 +1,6 @@
1
+ class Animal < ActiveRecord::Base
2
+ after_initialize :validate_name_presence, if: :new_record?
3
+ def validate_name_presence
4
+ raise ArgumentError if name.nil?
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Bike
2
+ def self.table_name_prefix
3
+ 'bike_'
4
+ end
5
+ class Maker < ActiveRecord::Base
6
+ end
7
+ end
data/test/models/tag.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Tag < ActiveRecord::Base
2
- self.primary_keys = :tag_id, :publisher_id
2
+ self.primary_keys = :tag_id, :publisher_id unless ENV["SKIP_COMPOSITE_PK"]
3
3
  has_many :books, inverse_of: :tag
4
4
  end
data/test/models/topic.rb CHANGED
@@ -1,9 +1,23 @@
1
1
  class Topic < ActiveRecord::Base
2
2
  validates_presence_of :author_name
3
3
  validates :title, numericality: { only_integer: true }, on: :context_test
4
+ validates :title, uniqueness: true
5
+ validates :content, uniqueness: true
6
+ validates :word_count, numericality: { greater_than: 0 }, if: :content?
7
+
8
+ validate -> { errors.add(:title, :validate_failed) if title == 'validate_failed' }
9
+ before_validation -> { errors.add(:title, :invalid) if title == 'invalid' }
4
10
 
5
11
  has_many :books, inverse_of: :topic
6
12
  belongs_to :parent, class_name: "Topic"
7
13
 
8
14
  composed_of :description, mapping: [%w(title title), %w(author_name author_name)], allow_nil: true, class_name: "TopicDescription"
15
+
16
+ default_scope { where(approved: true) }
17
+
18
+ private
19
+
20
+ def word_count
21
+ @word_count ||= content.to_s.scan(/\w+/).count
22
+ end
9
23
  end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :user_tokens, primary_key: :name, foreign_key: :user_name
3
+ end
@@ -0,0 +1,4 @@
1
+ class UserToken < ActiveRecord::Base
2
+ belongs_to :user, primary_key: :name, foreign_key: :user_name
3
+ validates :user, presence: true
4
+ end
@@ -159,14 +159,36 @@ ActiveRecord::Schema.define do
159
159
  t.string :Features
160
160
  end
161
161
 
162
+ create_table :users, force: :cascade do |t|
163
+ t.string :name, null: false
164
+ t.integer :lock_version, null: false, default: 0
165
+ end
166
+
167
+ create_table :user_tokens, force: :cascade do |t|
168
+ t.string :user_name, null: false
169
+ t.string :token, null: false
170
+ end
171
+
172
+ create_table :accounts, force: :cascade do |t|
173
+ t.string :name, null: false
174
+ t.integer :lock, null: false, default: 0
175
+ end
176
+
177
+ create_table :bike_makers, force: :cascade do |t|
178
+ t.string :name, null: false
179
+ t.integer :lock_version, null: false, default: 0
180
+ end
181
+
162
182
  add_index :cars, :Name, unique: true
163
183
 
164
- execute %(
165
- CREATE TABLE IF NOT EXISTS tags (
166
- tag_id INT NOT NULL,
167
- publisher_id INT NOT NULL,
168
- tag VARCHAR(50),
169
- PRIMARY KEY (tag_id, publisher_id)
170
- );
171
- ).split.join(' ').strip
184
+ unless ENV["SKIP_COMPOSITE_PK"]
185
+ execute %(
186
+ CREATE TABLE IF NOT EXISTS tags (
187
+ tag_id INT NOT NULL,
188
+ publisher_id INT NOT NULL,
189
+ tag VARCHAR(50),
190
+ PRIMARY KEY (tag_id, publisher_id)
191
+ );
192
+ ).split.join(' ').strip
193
+ end
172
194
  end
@@ -0,0 +1,19 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :books, force: :cascade do |t|
3
+ t.string :title, null: false
4
+ t.virtual :upper_title, type: :string, as: "upper(`title`)" if t.respond_to?(:virtual)
5
+ t.string :publisher, null: false, default: 'Default Publisher'
6
+ t.string :author_name, null: false
7
+ t.datetime :created_at
8
+ t.datetime :created_on
9
+ t.datetime :updated_at
10
+ t.datetime :updated_on
11
+ t.date :publish_date
12
+ t.integer :topic_id
13
+ t.integer :tag_id
14
+ t.integer :publisher_id
15
+ t.boolean :for_sale, default: true
16
+ t.integer :status, default: 0
17
+ t.string :type
18
+ end
19
+ end
@@ -1,12 +1,26 @@
1
1
  ActiveRecord::Schema.define do
2
2
  execute('CREATE extension IF NOT EXISTS "hstore";')
3
+ execute('CREATE extension IF NOT EXISTS "pgcrypto";')
3
4
  execute('CREATE extension IF NOT EXISTS "uuid-ossp";')
4
5
 
6
+ # create ENUM if it does not exist yet
7
+ begin
8
+ execute('CREATE TYPE vendor_type AS ENUM (\'wholesaler\', \'retailer\');')
9
+ rescue ActiveRecord::StatementInvalid => e
10
+ # since PostgreSQL does not support IF NOT EXISTS when creating a TYPE,
11
+ # rescue the error and check the error class
12
+ raise unless e.cause.is_a? PG::DuplicateObject
13
+ execute('ALTER TYPE vendor_type ADD VALUE IF NOT EXISTS \'wholesaler\';')
14
+ execute('ALTER TYPE vendor_type ADD VALUE IF NOT EXISTS \'retailer\';')
15
+ end
16
+
5
17
  create_table :vendors, id: :uuid, force: :cascade do |t|
6
18
  t.string :name, null: true
19
+ t.text :hours
7
20
  t.text :preferences
8
21
 
9
22
  if t.respond_to?(:json)
23
+ t.json :pure_json_data
10
24
  t.json :data
11
25
  else
12
26
  t.text :data
@@ -19,6 +33,7 @@ ActiveRecord::Schema.define do
19
33
  end
20
34
 
21
35
  if t.respond_to?(:jsonb)
36
+ t.jsonb :pure_jsonb_data
22
37
  t.jsonb :settings
23
38
  t.jsonb :json_data, null: false, default: {}
24
39
  else
@@ -26,6 +41,8 @@ ActiveRecord::Schema.define do
26
41
  t.text :json_data
27
42
  end
28
43
 
44
+ t.column :vendor_type, :vendor_type
45
+
29
46
  t.datetime :created_at
30
47
  t.datetime :updated_at
31
48
  end
@@ -35,6 +52,7 @@ ActiveRecord::Schema.define do
35
52
  t.column :alarm_type, :integer, null: false
36
53
  t.column :status, :integer, null: false
37
54
  t.column :metadata, :text
55
+ t.column :secret_key, :binary
38
56
  t.datetime :created_at
39
57
  t.datetime :updated_at
40
58
  end
@@ -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,12 +9,13 @@ 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
16
16
  sequence(:title) { |n| "Title #{n}" }
17
17
  sequence(:author_name) { |n| "Author #{n}" }
18
+ sequence(:content) { |n| "Content #{n}" }
18
19
  end
19
20
 
20
21
  factory :widget do
@@ -26,7 +27,7 @@ FactoryGirl.define do
26
27
 
27
28
  trait :with_rule do
28
29
  after(:build) do |question|
29
- question.build_rule(FactoryGirl.attributes_for(:rule))
30
+ question.build_rule(FactoryBot.attributes_for(:rule))
30
31
  end
31
32
  end
32
33
  end
@@ -39,21 +40,21 @@ FactoryGirl.define do
39
40
  factory :topic_with_book, parent: :topic do
40
41
  after(:build) do |topic|
41
42
  2.times do
42
- 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')
43
44
  3.times do
44
- book.chapters.build(title: FactoryGirl.generate(:chapter_title))
45
+ book.chapters.build(title: FactoryBot.generate(:chapter_title))
45
46
  end
46
47
 
47
48
  4.times do
48
- book.end_notes.build(note: FactoryGirl.generate(:end_note))
49
+ book.end_notes.build(note: FactoryBot.generate(:end_note))
49
50
  end
50
51
  end
51
52
  end
52
53
  end
53
54
 
54
55
  factory :book do
55
- title 'Tortilla Flat'
56
- author_name 'John Steinbeck'
56
+ title { 'Tortilla Flat' }
57
+ author_name { 'John Steinbeck' }
57
58
  end
58
59
 
59
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
@@ -64,8 +64,8 @@ def should_support_mysql_import_functionality
64
64
  let(:columns) { %w(id author_name title) }
65
65
 
66
66
  setup do
67
- topics << Topic.create!(title: "LDAP", author_name: "Big Bird")
68
- topics << Topic.create!(title: "Rails Recipes", author_name: "Elmo")
67
+ topics << Topic.create!(title: "LDAP", author_name: "Big Bird", content: "Putting Directories to Work.")
68
+ topics << Topic.create!(title: "Rails Recipes", author_name: "Elmo", content: "A trusted collection of solutions.")
69
69
  end
70
70
 
71
71
  it "synchronizes passed in ActiveRecord model instances with the data just imported" do
@@ -82,5 +82,17 @@ def should_support_mysql_import_functionality
82
82
  assert_equal "Chad Fowler", topics.last.author_name, "wrong author!"
83
83
  end
84
84
  end
85
+
86
+ if ENV['AR_VERSION'].to_f >= 5.1
87
+ context "with virtual columns" do
88
+ let(:books) { [Book.new(author_name: "foo", title: "bar")] }
89
+
90
+ it "ignores virtual columns and creates record" do
91
+ assert_difference "Book.count", +1 do
92
+ Book.import books
93
+ end
94
+ end
95
+ end
96
+ end
85
97
  end
86
98
  end
@@ -24,6 +24,36 @@ def should_support_postgresql_import_functionality
24
24
  end
25
25
  end
26
26
 
27
+ context "setting attributes and marking clean" do
28
+ let(:topic) { Build(:topics) }
29
+
30
+ setup { Topic.import([topic]) }
31
+
32
+ it "assigns ids" do
33
+ assert topic.id.present?
34
+ end
35
+
36
+ it "marks models as clean" do
37
+ assert !topic.changed?
38
+ end
39
+
40
+ if ENV['AR_VERSION'].to_f > 4.1
41
+ it "moves the dirty changes to previous_changes" do
42
+ assert topic.previous_changes.present?
43
+ end
44
+ end
45
+
46
+ it "marks models as persisted" do
47
+ assert !topic.new_record?
48
+ assert topic.persisted?
49
+ end
50
+
51
+ it "assigns timestamps" do
52
+ assert topic.created_at.present?
53
+ assert topic.updated_at.present?
54
+ end
55
+ end
56
+
27
57
  describe "with query cache enabled" do
28
58
  setup do
29
59
  unless ActiveRecord::Base.connection.query_cache_enabled
@@ -61,6 +91,96 @@ def should_support_postgresql_import_functionality
61
91
  assert_equal [], Book.import(books, no_returning: true).ids
62
92
  end
63
93
  end
94
+
95
+ describe "returning" do
96
+ let(:books) { [Book.new(author_name: "King", title: "It")] }
97
+ let(:result) { Book.import(books, returning: %w(author_name title)) }
98
+ let(:book_id) do
99
+ if RUBY_PLATFORM == 'java' || ENV['AR_VERSION'].to_i >= 5.0
100
+ books.first.id
101
+ else
102
+ books.first.id.to_s
103
+ end
104
+ end
105
+
106
+ it "creates records" do
107
+ assert_difference("Book.count", +1) { result }
108
+ end
109
+
110
+ it "returns ids" do
111
+ result
112
+ assert_equal [book_id], result.ids
113
+ end
114
+
115
+ it "returns specified columns" do
116
+ assert_equal [%w(King It)], result.results
117
+ end
118
+
119
+ context "when given an empty array" do
120
+ let(:result) { Book.import([], returning: %w(title)) }
121
+
122
+ setup { result }
123
+
124
+ it "returns empty arrays for ids and results" do
125
+ assert_equal [], result.ids
126
+ assert_equal [], result.results
127
+ end
128
+ end
129
+
130
+ context "when a returning column is a serialized attribute" do
131
+ let(:vendor) { Vendor.new(hours: { monday: '8-5' }) }
132
+ let(:result) { Vendor.import([vendor], returning: %w(hours)) }
133
+
134
+ it "creates records" do
135
+ assert_difference("Vendor.count", +1) { result }
136
+ end
137
+ end
138
+
139
+ context "when primary key and returning overlap" do
140
+ let(:result) { Book.import(books, returning: %w(id title)) }
141
+
142
+ setup { result }
143
+
144
+ it "returns ids" do
145
+ assert_equal [book_id], result.ids
146
+ end
147
+
148
+ it "returns specified columns" do
149
+ assert_equal [[book_id, 'It']], result.results
150
+ end
151
+ end
152
+
153
+ context "setting model attributes" do
154
+ let(:code) { 'abc' }
155
+ let(:discount) { 0.10 }
156
+ let(:original_promotion) do
157
+ Promotion.new(code: code, discount: discount)
158
+ end
159
+ let(:updated_promotion) do
160
+ Promotion.new(code: code, description: 'ABC discount')
161
+ end
162
+ let(:returning_columns) { %w(discount) }
163
+
164
+ setup do
165
+ Promotion.import([original_promotion])
166
+ Promotion.import([updated_promotion],
167
+ on_duplicate_key_update: { conflict_target: %i(code), columns: %i(description) },
168
+ returning: returning_columns)
169
+ end
170
+
171
+ it "sets model attributes" do
172
+ assert_equal updated_promotion.discount, discount
173
+ end
174
+
175
+ context "returning multiple columns" do
176
+ let(:returning_columns) { %w(discount description) }
177
+
178
+ it "sets model attributes" do
179
+ assert_equal updated_promotion.discount, discount
180
+ end
181
+ end
182
+ end
183
+ end
64
184
  end
65
185
 
66
186
  if ENV['AR_VERSION'].to_f >= 4.0
@@ -134,6 +254,41 @@ def should_support_postgresql_import_functionality
134
254
  assert_equal({}, Vendor.first.json_data)
135
255
  end
136
256
  end
257
+
258
+ %w(json jsonb).each do |json_type|
259
+ describe "with pure #{json_type} fields" do
260
+ let(:data) { { a: :b } }
261
+ let(:json_field_name) { "pure_#{json_type}_data" }
262
+ it "imports the values from saved records" do
263
+ vendor = Vendor.create!(name: 'Vendor 1', json_field_name => data)
264
+
265
+ Vendor.import [vendor], on_duplicate_key_update: [json_field_name]
266
+ assert_equal(data.as_json, vendor.reload[json_field_name])
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ describe "with enum field" do
273
+ let(:vendor_type) { "retailer" }
274
+ it "imports the correct values for enum fields" do
275
+ vendor = Vendor.new(name: 'Vendor 1', vendor_type: vendor_type)
276
+ assert_difference "Vendor.count", +1 do
277
+ Vendor.import [vendor]
278
+ end
279
+ assert_equal(vendor_type, Vendor.first.vendor_type)
280
+ end
281
+ end
282
+
283
+ describe "with binary field" do
284
+ let(:binary_value) { "\xE0'c\xB2\xB0\xB3Bh\\\xC2M\xB1m\\I\xC4r".force_encoding('ASCII-8BIT') }
285
+ it "imports the correct values for binary fields" do
286
+ alarms = [Alarm.new(device_id: 1, alarm_type: 1, status: 1, secret_key: binary_value)]
287
+ assert_difference "Alarm.count", +1 do
288
+ Alarm.import alarms
289
+ end
290
+ assert_equal(binary_value, Alarm.first.secret_key)
291
+ end
137
292
  end
138
293
  end
139
294
 
@@ -176,7 +331,7 @@ def should_support_postgresql_upsert_functionality
176
331
  # would be associated with the wrong parent.
177
332
  it ":on_duplicate_key_ignore is ignored" do
178
333
  assert_raise ActiveRecord::RecordNotUnique do
179
- Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true
334
+ Topic.import mixed_topics, recursive: true, on_duplicate_key_ignore: true, validate: false
180
335
  end
181
336
  end
182
337
  end
@@ -190,6 +345,30 @@ def should_support_postgresql_upsert_functionality
190
345
  end
191
346
 
192
347
  context "using a hash" do
348
+ context "with :columns :all" do
349
+ let(:columns) { %w( id title author_name author_email_address parent_id ) }
350
+ let(:updated_values) { [[99, "Book - 2nd Edition", "Jane Doe", "janedoe@example.com", 57]] }
351
+
352
+ macro(:perform_import) do |*opts|
353
+ Topic.import columns, updated_values, opts.extract_options!.merge(on_duplicate_key_update: { conflict_target: :id, columns: :all }, validate: false)
354
+ end
355
+
356
+ setup do
357
+ values = [[99, "Book", "John Doe", "john@doe.com", 17, 3]]
358
+ Topic.import columns + ['replies_count'], values, validate: false
359
+ end
360
+
361
+ it "should update all specified columns" do
362
+ perform_import
363
+ updated_topic = Topic.find(99)
364
+ assert_equal 'Book - 2nd Edition', updated_topic.title
365
+ assert_equal 'Jane Doe', updated_topic.author_name
366
+ assert_equal 'janedoe@example.com', updated_topic.author_email_address
367
+ assert_equal 57, updated_topic.parent_id
368
+ assert_equal 3, updated_topic.replies_count
369
+ end
370
+ end
371
+
193
372
  context "with :columns a hash" do
194
373
  let(:columns) { %w( id title author_name author_email_address parent_id ) }
195
374
  let(:values) { [[99, "Book", "John Doe", "john@doe.com", 17]] }
@@ -204,6 +383,13 @@ def should_support_postgresql_upsert_functionality
204
383
  @topic = Topic.find 99
205
384
  end
206
385
 
386
+ it "should not modify the passed in :on_duplicate_key_update columns array" do
387
+ assert_nothing_raised do
388
+ columns = %w(title author_name).freeze
389
+ Topic.import columns, [%w(foo, bar)], { on_duplicate_key_update: { columns: columns }.freeze }.freeze
390
+ end
391
+ end
392
+
207
393
  context "using string hash map" do
208
394
  let(:update_columns) { { "title" => "title", "author_email_address" => "author_email_address", "parent_id" => "parent_id" } }
209
395
  should_support_on_duplicate_key_update
@@ -273,6 +459,39 @@ def should_support_postgresql_upsert_functionality
273
459
  end
274
460
  end
275
461
 
462
+ context 'with :condition' do
463
+ let(:columns) { %w( id device_id alarm_type status metadata) }
464
+ let(:values) { [[99, 17, 1, 1, 'foo']] }
465
+ let(:updated_values) { [[99, 17, 1, 1, 'bar']] }
466
+
467
+ macro(:perform_import) do |*opts|
468
+ Alarm.import(
469
+ columns,
470
+ updated_values,
471
+ opts.extract_options!.merge(
472
+ on_duplicate_key_update: {
473
+ conflict_target: [:id],
474
+ condition: "alarms.metadata NOT LIKE '%foo%'",
475
+ columns: [:metadata]
476
+ },
477
+ validate: false
478
+ )
479
+ )
480
+ end
481
+
482
+ macro(:updated_alarm) { Alarm.find(@alarm.id) }
483
+
484
+ setup do
485
+ Alarm.import columns, values, validate: false
486
+ @alarm = Alarm.find 99
487
+ end
488
+
489
+ it 'should not update fields not matched' do
490
+ perform_import
491
+ assert_equal 'foo', updated_alarm.metadata
492
+ end
493
+ end
494
+
276
495
  context "with :constraint_name" do
277
496
  let(:columns) { %w( id title author_name author_email_address parent_id ) }
278
497
  let(:values) { [[100, "Book", "John Doe", "john@doe.com", 17]] }