How to perform an upsert without loading associations?

Assume an Author schema:

schema "authors" do
    field :name, :string
    has_many :posts, Post
  end

  def changeset(struct, attrs) do
    struct
    |> cast(attrs, [:name])
  end

I can perform an update:

author |> Author.changeset(%{name: "Jo"}) |> Blog.Repo.update!()    

but an upsert fails:

 author |> Author.changeset(%{name: "Jo"}) |> Blog.Repo.insert!(on_conflict: {:replace_all_except, [:id]})
** (RuntimeError) attempting to cast or change association `posts` from `Author` that was not loaded. Please preload your associations before manipulating them through changesets

Is it possible to perform an upsert without loading the associations?
Or is this by design?

Have you taken a look at Repo.insert_all/3? They have a full section on upsert, maybe that helps.

Something like

query = Author |> where([a], a.id == ^id) |> select([a], %{name: "Jo"})
Repo.insert_all(Author, query, on_conflict: : replace_all)

Maybe Repo.insert_or_update is what you’re looking for. A changeset is already aware if you loaded author from the db or not.

The code that implements replace_all_except starts from the schema’s list of fields:

And that’s a list of built from all the field calls inside the schema block - associations aren’t in there.

Can you post the stacktrace of the error and/or the full source of Author.changeset/2?

Thanks for all the feedback. Along with a discussion on the Elxir discord I’ve improved my understanding of how upsert works.

What I should be doing is this:

%Author{} |> Author.changeset(%{name: "Jo"}) |> Blog.Repo.insert!(on_conflict: {:replace_all_except, [:id]})

If the struct metadata state is built then it works fine. If it’s loaded then the associations are expected.

I want to avoid loading it first, and just insert or update an existing record, so using a built struct is the best approach.

If I had the struct already loaded I can use insert_or_update