diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index fb7ad83126a..090c4feea79 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -240,10 +240,7 @@ module HasCustomFields if v.is_a?(Array) && field_type != :json v.each { |subv| _custom_fields.create!(name: k, value: subv) } else - _custom_fields.create!( - name: k, - value: v.is_a?(Hash) || field_type == :json ? v.to_json : v - ) + create_singular(k, v, field_type) end end end @@ -252,7 +249,16 @@ module HasCustomFields end end - protected + def create_singular(name, value, field_type = nil) + write_value = value.is_a?(Hash) || field_type == :json ? value.to_json : value + _custom_fields.create!(name: name, value: write_value) + rescue ActiveRecord::RecordNotUnique + # We support unique indexes on certain fields. In the event two concurrenct processes attempt to + # update the same custom field we should catch the error and perform an update instead. + _custom_fields.where(name: name).update_all(value: write_value) + end + +protected def refresh_custom_fields_from_db target = Hash.new diff --git a/spec/components/concern/has_custom_fields_spec.rb b/spec/components/concern/has_custom_fields_spec.rb index 324ba16a62d..27c7e989084 100644 --- a/spec/components/concern/has_custom_fields_spec.rb +++ b/spec/components/concern/has_custom_fields_spec.rb @@ -8,6 +8,10 @@ describe HasCustomFields do before do DB.exec("create temporary table custom_fields_test_items(id SERIAL primary key)") DB.exec("create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text)") + DB.exec(<<~SQL) + CREATE UNIQUE INDEX ON custom_fields_test_item_custom_fields (custom_fields_test_item_id) + WHERE NAME = 'rare' + SQL class CustomFieldsTestItem < ActiveRecord::Base include HasCustomFields @@ -28,7 +32,7 @@ describe HasCustomFields do Object.send(:remove_const, :CustomFieldsTestItemCustomField) end - it "simple modification of custom fields" do + it "allows simple modification of custom fields" do test_item = CustomFieldsTestItem.new expect(test_item.custom_fields["a"]).to eq(nil) @@ -67,7 +71,7 @@ describe HasCustomFields do expect(test_item.custom_fields["a"]).to eq("0") end - it "reload loads from database" do + it "reloads from the database" do test_item = CustomFieldsTestItem.new test_item.custom_fields["a"] = 0 @@ -87,7 +91,7 @@ describe HasCustomFields do expect(test_item.custom_fields["a"]).to eq("1") end - it "double save actually saves" do + it "actually saves on double save" do test_item = CustomFieldsTestItem.new test_item.custom_fields = { "a" => "b" } test_item.save @@ -165,7 +169,7 @@ describe HasCustomFields do expect((before_ids - after_ids).size).to eq(1) end - it "simple modifications don't interfere" do + it "doesn't allow simple modifications to interfere" do test_item = CustomFieldsTestItem.new expect(test_item.custom_fields["a"]).to eq(nil) @@ -214,7 +218,6 @@ describe HasCustomFields do end it "will not fail to load custom fields if json is corrupt" do - field_type = "bad_json" CustomFieldsTestItem.register_custom_field_type(field_type, :json) @@ -268,6 +271,23 @@ describe HasCustomFields do expect(test_item.reload.custom_fields).to eq(expected) end + describe "create_singular" do + it "creates new records" do + item = CustomFieldsTestItem.create! + item.create_singular('hello', 'world') + expect(item.reload.custom_fields['hello']).to eq('world') + end + + it "upserts on a database constraint error" do + item0 = CustomFieldsTestItem.new + item0.custom_fields = { "rare" => "gem" } + item0.save + + item0.create_singular('rare', "diamond") + expect(item0.reload.custom_fields['rare']).to eq("diamond") + end + end + describe "upsert_custom_fields" do it 'upserts records' do test_item = CustomFieldsTestItem.create