activerecord-import 0.17.2 → 1.1.0

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