avram icon indicating copy to clipboard operation
avram copied to clipboard

If validations set upsert lookup column value, existing record is not found

Open wezm opened this issue 3 years ago • 3 comments

I have a scenario where my model has a url : String field, which is the lookup column upsert_lookup_columns :url. This field is set indirectly through an attribute on the save operation:

  attribute project : String

  before_save validate_format_of project, with: /^[a-z0-9_-]+\/[a-z0-9_-]+$/i
  before_save validate_project

  def validate_project
    # simplifed example
    url.value = "https://example.com/#{project.value}"
  end

For this model upsert is not working properly. It always attempts to insert a new record, resulting in a unique key violation.

The issue seems to be that the upsert method tries to retrieve the existing record before the validations have been run, and thus in my case url is still nil:

https://github.com/luckyframework/avram/blob/0ea2a6140f5f9ac770eeaa5badf1d479a5b60f69/src/avram/upsert.cr#L72-L82

It does this query:

SELECT feeds.id, feeds.created_at, feeds.updated_at, feeds.refreshed_at, feeds.title, feeds.url, feeds.etag, feeds.last_modified, feeds.last_refresh_hash, feeds.source_slug FROM feeds WHERE feeds.url IS NULL ORDER BY feeds.id ASC LIMIT 1

Note the WHERE feeds.url IS NULL bit.

It gets no record and then proceeds to call save, which attempts to insert a new record. However, as part of save it first calls valid?, which in turn populates url and then when it comes to persist it, a duplicate row is attempted to be inserted.

wezm avatar Jan 01 '23 02:01 wezm

oh that's interesting. I've actually seen a few unique constraint errors in my app, and never knew where they were coming from, but I think it's from an upsert because we use these quite a bit.

I guess this is a bit of a tricky one... If you have a before_save validate_these_are_required, and you haven't looked up the record yet, you may get validation errors. But then on the flip side, if you do the lookup first, and haven't set your values yet, then you'll run in to this....

Does this need a new before_lookup callback? Or do we just say that in cases like this, you need to pass a named arg for the lookup column? :thinking:

jwoertink avatar Jan 02 '23 19:01 jwoertink

Or do we just say that in cases like this, you need to pass a named arg for the lookup column? 🤔

This would certainly be the easiest way forward for now.

wezm avatar Jan 02 '23 22:01 wezm

I was just looking in to this more and it's pretty tricky. We would have to first move the run_default_validations and before_save calls out of the save method, then add those in before every call to save that happens all over. Then we run the issue of validations being ran twice on upserts when a record is found. I think it would be a breaking change too since you could no longer do this

op = SaveThing.new
op.whatever.value = "something"
# no validations or before_save would get ran....
op.save

Alternatively, we could have the upsert work a little different, and duplicate some of the logic over just in a different order. That could get messy though. I think I want to sit on this a little longer until I can think of a cleaner way to handle this.

jwoertink avatar Dec 29 '23 21:12 jwoertink