A typical Changeset with upsert identity

I feel like a sheep for not understanding this.
This is for educational purpose, as I can do Manual Action, but I i figure this is shorter:

Enum.map(Enum.zip(params["warehouse"]["name"], params["warehouse"]["type"]), fn pos ->
      Warehouse
    |> Ash.Changeset.new()
    |> Ash.Changeset.for_create(:create, %{warehouse_code: elem(pos, 0), warehouse_type: elem(pos, 1)}, upsert?: true, upsert_identity: :unique_record, upsert_fields: :replace_all)
    |> Ash.Changeset.set_tenant("org_#{company.id}")
    |> MetacrisisBi.Warehouse.create!()
    end)

I tried upsert_fields also giving a specific attribute of the resource but also doesn’t work.

Wlp! :slight_smile:

In what way does it not work? What behavior are you expecting vs what behavior is it exhibiting?

Sorry for not putting the whole situation inside Zach!
Thank you for taking your time! :pray:
So, I tried different strategies (putting upsert in actions vs putting in the for_change Changeset function etc), but couldn’t get it.
This is the latest example where I am defaulting I presume to :update_all, but Ecto is complaining that set() is empty, which shouldn’t be as I am passing arguments to it?

Is there an Ash way specifically to force the Ecto.set?
I presumed that all arguments are passed automatically?

(In the given example I already have data in the table that have the same Warehouse_CODE, (using identity for uniqueness and to force upsert)

Here is the Changeset :

    Enum.map(Enum.zip(params["warehouse"]["code"], params["warehouse"]["type"]), fn pos ->
      Warehouse
      |> Ash.Changeset.new()
      |> Ash.Changeset.for_create(
        :create,
        %{warehouse_code: elem(pos, 0), warehouse_name: "Heho", warehouse_type: elem(pos, 1)},
        upsert?: true,
        upsert_identity: :unique_record,
        upsert_fields: [:replace_all])
      |> Ash.Changeset.set_tenant("org_#{company.id}")
      |> MetacrisisBi.Warehouse.create!()
    end)

        IO.inspect(new_warehouse)

        Warehouse
        |> Ash.Changeset.for_create(:create, new_warehouse)
        |> Ash.Changeset.set_tenant("org_#{company.id}")
        |> MetacrisisBi.Warehouse.create!()

Then from Warehouse resource:

  attributes do
    # Add an autogenerated UUID primary key called `:id`.
    uuid_primary_key(:id)
    attribute(:warehouse_name, :string)
    attribute(:warehouse_code, :string)
    attribute(:warehouse_type, :string)
  end
  identities do
    identity(:unique_record, [:warehouse_code])
  end

  code_interface do
    define_for(MetacrisisBi.Warehouse)
    define(:create, args: [:warehouse_name, :warehouse_code, :warehouse_type], action: :create)
    define(:read, action: :read)
    define(:update, action: :update)
    define(:destroy, action: :destroy)
  end

in actions:

    create :create do
      primary? true
      upsert? true
      upsert_identity(:unique_record)
    end

The error I get is:

[error] GenServer #PID<0.647.0> terminating
** (Ash.Error.Unknown) Unknown Error

* ** (Ecto.QueryError) `update_all` requires at least one field to be updated in query:

from r0 in MetacrisisBi.Warehouse.Record,
  update: [set: []]

    (ash 2.15.7) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
    (elixir 1.15.4) lib/enum.ex:1693: Enum."-map/2-lists^map/1-1-"/2
    (metacrisis_bi 0.1.0) lib/metacrisis_bi_web/warehouse/warehouse.ex:140: MetacrisisBiWeb.WarehouseLive.handle_event/3

Also just for another approach that I thought thought might work.

If I clean up the actions create :create, and just use the default action.

And I just put all the upsert and upsert identity in the Changeset:

create_warehouse = Warehouse
      |> Ash.Changeset.new()
      |> Ash.Changeset.for_create(
        :create,
        %{warehouse_code: elem(pos, 0), warehouse_name: "Heho", warehouse_type: elem(pos, 1)},
        upsert?: true,
        upsert_identity: :unique_record
      )
      |> Ash.Changeset.set_tenant("org_#{company.id}")
      |> MetacrisisBi.Warehouse.create!()

Then the error is alike:

[error] GenServer #PID<0.1846.0> terminating
** (Ash.Error.Unknown) Unknown Error

* ** (Postgrex.Error) ERROR 42P10 (invalid_column_reference) there is no unique or exclusion constraint matching the ON CONFLICT specification
    (ash 2.15.7) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
    (metacrisis_bi 0.1.0) lib/metacrisis_bi_web/warehouse/warehouse.ex:160: anonymous fn/2 in MetacrisisBiWeb.WarehouseLive.handle_event/3
    (elixir 1.15.4) lib/enum.ex:1693: Enum."-map/2-lists^map/1-1-"/2

If I add the upsert_fields [:replace_all] as well to the Changeset

Then its the same Ecto set empty erorr:

[error] GenServer #PID<0.1914.0> terminating
** (Ash.Error.Unknown) Unknown Error

* ** (Ecto.QueryError) `update_all` requires at least one field to be updated in query:

from r0 in MetacrisisBi.Warehouse.Record,
  update: [set: []]

    (ash 2.15.7) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
    (metacrisis_bi 0.1.0) lib/metacrisis_bi_web/warehouse/warehouse.ex:161: anonymous fn/2 in MetacrisisBiWeb.WarehouseLive.handle_event/3
    (elixir 1.15.4) lib/enum.ex:1693: Enum."-map/2-lists^map/1-1-"/2

Edit> Solved it by running read from DB, mapping in code and running :update on the found items, and :create for new ones, but would be cool to learn the basic usecase for upsert in this code context. I have upsert and identity running well for some bulk imports, but here its givin me some tough love.

I think the problem here is that we aren’t validating the value well enough :laughing:

upsert_fields :replace_all is what you want. upsert_fields [:replace_all] says “replace the fields [:replace_all]” and we filter for only attributes at some point.

1 Like