46

How do you create a unique index on two columns in Ecto, which would correspond to this:

CREATE TABLE someTable (
    col1 int NOT NULL,
    col2 int NOT NULL,
    primary key (col1, col2)
)

?

a_b
  • 1,828
  • 5
  • 23
  • 37

4 Answers4

126

A Little follow up on Patrick's answer

Using only create unique_index on your model will ultimately throw an exception instead of giving you an error.

To get an error add a constraint on your changeset but as a paremeter you can give the index name created by unique_index.

So in your migration file :

create unique_index(:your_table, [:col1, :col2], name: :your_index_name)

Then in your changeset :

def changeset(model, param \\ :empty) do
  model
  |> cast(params, @required_fields, @optional_fields)
  |> unique_constraint(:name_your_constraint, name: :your_index_name)
end
wintermeyer
  • 8,178
  • 8
  • 39
  • 85
TheSquad
  • 7,385
  • 8
  • 40
  • 79
  • 3
    This should be marked as the accepted answer imo. @TheSquad, does :name_your_constraint need to be either :col1 or :col2 in this context? Reference: https://hexdocs.pm/ecto/Ecto.Changeset.html#unique_constraint/3 – Olshansky Dec 01 '16 at 16:59
  • 7
    absolutely not, you can put any atom you want to have a comprehensive error message when the constraint is not met – TheSquad Dec 01 '16 at 17:02
  • The second parameter to `unique_constraint` (`:name_your_constraint` in the example) is meant as the name(s) of the column(s). You can use some fantasy name but if you want the error to appear at the right place e.g. in a Phoenix form you should take the name of the column/form field to which the error belongs. – TNT Dec 30 '22 at 17:59
13

You can create a unique index across multiple rows with

create unique_index(:some_table, [:col1, :col2])

I suppose if you want to have composite keys, you need to use execute/1 to run your SQL manually. Not sure how well composite keys work with Ecto though, I generally just stick with the standard serial id per table.

If you should go with the composite key approach, I think that the NOT NULL constraints are not necessary. The composite key should already enforce that the columns are not null.

Patrick Oscity
  • 53,604
  • 17
  • 144
  • 168
7

unique_index/3 would not create a composite primary key as is shown in the question's example. It does create a unique constraint.

If you do want to create a composite primary key (note: not recommended when working with Ecto), there's more information here:

Migration:

defmodule HelloPhoenix.Repo.Migrations.CreatePlayer do
  use Ecto.Migration

  def change do
    create table(:players, primary_key: false) do
      add :first_name, :string, primary_key: true
      add :last_name, :string, primary_key: true
      add :position, :string
      add :number, :integer
      ...

Schema:

defmodule HelloPhoenix.Player do
  use Ecto.Schema

  @primary_key false
  schema "players" do
    field :first_name, :string, primary_key: true
    field :last_name, :string, primary_key: true
    field :position, :string
    field :number, :integer
    ...

In most cases unique_index/3 is what you want, though.

Oliver
  • 3,981
  • 2
  • 21
  • 35
3

This is an old post, but I ran into this today. Remember that a unique constraint is not the same as a unique index (at least, not in all versions of PostGres -- there seems to be some movement on this). In Ecto, it seems that currently the only way to add a constraint that is not an index is to use execute/1 in your migration, e.g.

defmodule YourMigration do
  use Ecto.Migration

  def change do
    execute(
      "ALTER TABLE pre.yourtable ADD CONSTRAINT something UNIQUE (something)"
    )
  end
end
Everett
  • 8,746
  • 5
  • 35
  • 49