FIX: Upsert a custom field if a unique constraint fails

FIX: Upsert a custom field if a unique constraint fails

diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb
index fb7ad83..090c4fe 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 324ba16..27c7e98 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

GitHub sha: 72bac61c

1 Like

This commit has been mentioned on Discourse Meta. There might be relevant details there: