Ecto Changeset unique_constraint/3 for clustered index

How do you implement unique_constraint/3 for a clustered index?

If I have a unique index on the below table, how do I call unique_constraint in my changeset? I ask because the function typespec requires an atom as the field term, but a clustered index is a list.

unique_constraint(Ecto.Changeset.t, atom, Keyword.t)

create table(:letters)
  add :a, :string
  add :b, :string
  add :c, :string
end
create unique_index(:letters, [:a, :b], name: :letters_unique_index)

def changeset(struct, params \\ %{})
  struct
  |> cast(params, [:a, :b, :c])
  |> unique_constraint(???, name: :letters_unique_index)
end

Thanks!

1 Like

AFAIK, the first argument for unique_contraint/3 will be used just for the errors map creation. You can use one of :a or :b if you want the error to show on your form, or even make up your own like :unique and read it in the changeset.errors[:unique].

PS.: @josevalim maybe we could explain this situation clearly in the Ecto.Changeset.unique_contstraint/3 documentation, it caused the same confusion to me sometime ago.

1 Like

I tried this, but the shell still renders an Ecto.ConstraintError error when I run tests to check the constraint. I tried using a random atom, :uniq to identify the clustered unique index, and also the atom of the first field in the index. Unfortunately, neither produces a changeset error.

Could you please send a pull request or, if you can’t for some reason, open
up an issue? Thank you!

1 Like

Well, I tried here and everything worked as I mentioned:

# migration
create table(:team_memberships) do
  add :team_id, references(:teams, on_delete: :delete_all)
  add :user_id, references(:users, on_delete: :delete_all)

  timestamps()
end

create unique_index(:team_memberships, [:team_id, :user_id],
                    name: :team_memberships_relation_index)

# model
def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, [:team_id, :user_id])
  |> unique_constraint(:user_id, name: :team_memberships_relation_index)
end

With the above code, the following twice in the shell:

Hippo.insert(TeamMembership.changeset(%TeamMembership{}, %{team_id: 1, user_id: 1}))

Returned me a tuple:

{:error,
 Ecto.Changeset<action: :insert, changes: %{team_id: 1, user_id: 1},
  errors: [user_id: {"has already been taken", []}],
  data: TeamMembership<>, valid?: false>}

PR sent: Improving complex unique_constraint docs by kelvinst · Pull Request #1801 · elixir-ecto/ecto · GitHub

Oh good! I must be doing something wrong. I’ll give it another shot.

I got it working, thanks!

1 Like