Found a bug in Ecto changeset unique_constraint function

I have a table in which I have put unique constraints on the combination of two fields. I had also defined this in changeset.

def changeset(asset, attrs) do
    |> cast(attrs, @create_attrs)
    |> validate_required(@required_attrs)
    |> unique_constraint([:serial_number, :organization_id])

But I keep getting this error instead of the object {:error, #Ecto.Changeset…} -

Server: localhost:4000 (http)
Request: POST /api/create_asset
** (exit) an exception was raised:
    ** (Ecto.ConstraintError) constraint error when attempting to insert struct:

    * "assets_organization_id_serial_number_index" (unique_constraint)

If you would like to stop this constraint violation from raising an
exception and instead add it as an error to your changeset, please
call `unique_constraint/3` on your changeset with the constraint
`:name` as an option.

Until I found out that order also matters when writing the unique_constraint. It worked when i changed it to -

unique_constraint([:organization_id, :serial_number])

So I just wanted to put it here, so that we can fix this bug in the new release. Would also love to contribute if I’m able to fix it by myself.


This isn’t a bug, the “order” matters at the database level too. Ecto is using :org_id and :serial_number to create the name of the index. Otherwise, the order of compound indexes, at least in postgres, does matter.


The default behavior of unique_constraint is tripping you up here:

  • Putting organization_id first in the index makes sense since Postgres can use partial indexes from the left-hand side.
  • But putting serial_number first in the unique_constraint call makes sense too, since you likely want the “is already taken” message on the serial number rather than the organization ID.

One or the other of these has to change. Either:

  • tell unique_constraint the correct name of the index to use with the name: "assets_organization_id_serial_number_index" option
  • pass the fields to unique_constraint in the same order as the index (so the name can be automatically generated), then tell it to put the error on serial_number instead with error_key

Thanks @sodapopcan and @al2o3cr It makes sense.