Skip to content

Commit

Permalink
Add upsert_keys and upsert_options as opts
Browse files Browse the repository at this point in the history
This allows setting of `upsert_keys` and `upsert_options` directly when
calling `#upsert` or `.upsert`. This will overwrite the default
`upsert_keys` and `upsert_options` set in the model.
  • Loading branch information
hahuang65 committed May 1, 2019
1 parent afa65d6 commit 7ee24bf
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 15 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ class Account < ApplicationRecord
end
```

Overriding the models' upsert_keys can be done when calling #upsert:

```ruby
Account.upsert(attrs, opts: { upsert_keys: [:foo, :bar] })
# Or, on an instance:
account = Account.new(attrs)
account.upsert(opts: { upsert_keys: [:foo, :bar] })
```

## Tests

Make sure to have an upsert_test database:
Expand Down
17 changes: 9 additions & 8 deletions lib/active_record_upsert/active_record/persistence.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module ActiveRecordUpsert
module ActiveRecord
module PersistenceExtensions
def upsert!(attributes: nil, arel_condition: nil, validate: true)
def upsert!(attributes: nil, arel_condition: nil, validate: true, opts: {})
raise ::ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
raise ::ActiveRecord::RecordSavedError, "Can't upsert a record that has already been saved" if persisted?
validate == false || perform_validations || raise_validation_error
Expand All @@ -11,7 +11,7 @@ def upsert!(attributes: nil, arel_condition: nil, validate: true)
attributes = attributes +
timestamp_attributes_for_create_in_model +
timestamp_attributes_for_update_in_model
_upsert_record(attributes.map(&:to_s).uniq, arel_condition)
_upsert_record(attributes.map(&:to_s).uniq, arel_condition, opts)
}
}

Expand All @@ -24,10 +24,10 @@ def upsert(*args)
false
end

def _upsert_record(upsert_attribute_names = changed, arel_condition = nil)
def _upsert_record(upsert_attribute_names = changed, arel_condition = nil, opts = {})
existing_attribute_names = attributes_for_create(attributes.keys)
existing_attributes = attributes_with_values(existing_attribute_names)
values = self.class._upsert_record(existing_attributes, upsert_attribute_names, [arel_condition].compact)
values = self.class._upsert_record(existing_attributes, upsert_attribute_names, [arel_condition].compact, opts)
@attributes = self.class.attributes_builder.build_from_database(values.first.to_h)
@new_record = false
values
Expand All @@ -40,12 +40,12 @@ def upsert_operation
end

module ClassMethods
def upsert!(attributes, arel_condition: nil, validate: true, &block)
def upsert!(attributes, arel_condition: nil, validate: true, opts: {}, &block)
if attributes.is_a?(Array)
attributes.collect { |hash| upsert(hash, &block) }
else
new(attributes, &block).upsert!(
attributes: attributes.keys, arel_condition: arel_condition, validate: validate
attributes: attributes.keys, arel_condition: arel_condition, validate: validate, opts: opts
)
end
end
Expand All @@ -56,8 +56,9 @@ def upsert(*args)
false
end

def _upsert_record(existing_attributes, upsert_attributes_names, wheres) # :nodoc:
upsert_keys = self.upsert_keys || [primary_key]
def _upsert_record(existing_attributes, upsert_attributes_names, wheres, opts) # :nodoc:
upsert_keys = opts[:upsert_keys] || self.upsert_keys || [primary_key]
upsert_options = opts[:upsert_options] || self.upsert_options
upsert_attributes_names = upsert_attributes_names - [*upsert_keys, 'created_at']

existing_attributes = existing_attributes
Expand Down
11 changes: 6 additions & 5 deletions lib/active_record_upsert/compatibility/rails51.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
module ActiveRecordUpsert
module ActiveRecord
module PersistenceExtensions
def _upsert_record(upsert_attribute_names = changed, arel_condition = nil)
def _upsert_record(upsert_attribute_names = changed, arel_condition = nil, opts = {})
upsert_attribute_names = upsert_attribute_names.map { |name| _prepare_column(name) } & self.class.column_names
existing_attributes = arel_attributes_with_values_for_create(self.class.column_names)
values = self.class.unscoped.upsert(existing_attributes, upsert_attribute_names, [arel_condition].compact)
values = self.class.unscoped.upsert(existing_attributes, upsert_attribute_names, [arel_condition].compact, opts)
@new_record = false
@attributes = self.class.attributes_builder.build_from_database(values.first.to_h)
values
Expand All @@ -20,9 +20,10 @@ def _prepare_column(name)
end

module RelationExtensions
def upsert(existing_attributes, upsert_attributes, wheres) # :nodoc:
def upsert(existing_attributes, upsert_attributes, wheres, opts) # :nodoc:
substitutes, binds = substitute_values(existing_attributes)
upsert_keys = self.klass.upsert_keys || [primary_key]
upsert_keys = opts[:upsert_keys] || self.klass.upsert_keys || [primary_key]
upsert_options = opts[:upsert_options] || self.klass.upsert_options

upsert_attributes = upsert_attributes - [*upsert_keys, 'created_at']
upsert_keys_filter = ->(o) { upsert_attributes.include?(o.name) }
Expand All @@ -34,7 +35,7 @@ def upsert(existing_attributes, upsert_attributes, wheres) # :nodoc:

on_conflict_do_update = ::Arel::OnConflictDoUpdateManager.new
on_conflict_do_update.target = target
on_conflict_do_update.target_condition = self.klass.upsert_options[:where]
on_conflict_do_update.target_condition = upsert_options[:where]
on_conflict_do_update.wheres = wheres
on_conflict_do_update.set(vals_for_upsert)

Expand Down
40 changes: 40 additions & 0 deletions spec/active_record/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ module ActiveRecord
expect(upserted.name).to eq(nil)
end
end

context 'with opts' do
let(:attrs) { {make: 'Ford', name: 'Focus', year: 2017 } }
let!(:vehicle) { Vehicle.create(attrs) }

context 'with upsert_keys' do
it 'allows upsert_keys to be set when #upsert is called' do
upserted = Vehicle.new({ make: 'Volkswagen', name: 'Golf', year: attrs[:year] })
expect { upserted.upsert(opts: { upsert_keys: [:year] }) }.not_to change { Vehicle.count }.from(1)
expect(upserted.id).to eq(vehicle.id)
end
end

context 'with upsert_options' do
it 'allows upsert_options to be set when #upsert is called' do
upserted = Vehicle.new({ make: attrs[:make], name: 'GT', wheels_count: 4 })
expect { upserted.upsert(opts: { upsert_keys: [:make], upsert_options: { where: 'year IS NULL' } }) }.to change { Vehicle.count }.from(1).to(2)
expect(upserted.id).not_to eq(vehicle.id)
end
end
end
end

context 'when the record is not new' do
Expand Down Expand Up @@ -172,6 +193,25 @@ module ActiveRecord
expect(existing.reload.updated_at).to be > existing_updated_at
end
end

context 'with opts' do
let(:attrs) { {make: 'Ford', name: 'Focus', year: 2017 } }
let!(:vehicle) { Vehicle.create(attrs) }

context 'with upsert_keys' do
it 'allows upsert_keys to be set when .upsert is called' do
expect { Vehicle.upsert({ make: 'Volkswagen', name: 'Golf', year: attrs[:year] }, opts: { upsert_keys: [:year] }) }.not_to change { Vehicle.count }.from(1)
expect(vehicle.reload.make).to eq('Volkswagen')
end
end

context 'with upsert_options' do
it 'allows upsert_options to be set when #upsert is called' do
expect { Vehicle.upsert({ make: attrs[:make], name: 'GT', wheels_count: 4 }, opts: { upsert_keys: [:make], upsert_options: { where: 'year IS NULL' } }) }.to change { Vehicle.count }.from(1).to(2)
expect(vehicle.reload.wheels_count).to be_nil
end
end
end
end

context 'with assocations' do
Expand Down
1 change: 1 addition & 0 deletions spec/active_record/key_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ module ActiveRecord
expect(upserted.wheels_count).to eq(1)
end
end

context 'different ways of setting keys' do
let(:attrs) { {make: 'Ford', name: 'Focus', long_field: SecureRandom.uuid} }
let!(:vehicule) { Vehicle.create(attrs) }
Expand Down
7 changes: 7 additions & 0 deletions spec/dummy/db/migrate/20190428142610_add_year_to_vehicles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddYearToVehicles < ActiveRecord::Migration[5.0]
def change
add_column :vehicles, :year, :integer
add_index :vehicles, :year, unique: true
add_index :vehicles, [:make], unique: true, where: "year IS NULL", name: 'partial_index_vehicles_on_make_without_year'
end
end
20 changes: 18 additions & 2 deletions spec/dummy/db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ CREATE TABLE vehicles (
make character varying,
long_field character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
updated_at timestamp without time zone NOT NULL,
year integer
);


Expand Down Expand Up @@ -238,6 +239,20 @@ CREATE UNIQUE INDEX index_vehicles_on_make_and_name ON vehicles USING btree (mak
CREATE UNIQUE INDEX index_vehicles_on_md5_long_field ON vehicles USING btree (md5((long_field)::text));


--
-- Name: index_vehicles_on_year; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX index_vehicles_on_year ON vehicles USING btree (year);


--
-- Name: partial_index_vehicles_on_make_without_year; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX partial_index_vehicles_on_make_without_year ON vehicles USING btree (make) WHERE (year IS NULL);


--
-- PostgreSQL database dump complete
--
Expand All @@ -247,6 +262,7 @@ SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20160419103547'),
('20160419124138'),
('20160419124140');
('20160419124140'),
('20190428142610');


0 comments on commit 7ee24bf

Please sign in to comment.