Francesco

Francesco

Hello!

I have a user table where I don’t allow two verified users with the same email to exist. This is enforced by a unique constraint on the table and a corresponding unique constraint on the changeset. When this case occurs I will get back a changeset that looks like:

#Ecto.Changeset<
  action: :insert,
  changes: %{...},
  errors: [
    email: {"has already been taken",
     [constraint: :unique, constraint_name: "users_verified_email_index"]}
  ],
  data: #Scribe.Accounts.User<>,
  valid?: false
>

I want to pattern match on this particular error constraint however this is a bit of a full on pattern match and it involves keyword list which means I have to get the number and order of items to match, i.e.

{:error, %Ecto.Changeset{ errors: [email: {_, [constraint: :unique, constraint_name: _]}] }} ->

Is there a better way to pattern match on these error constraints? Or is there an alternative way of approaching the problem?

Perhaps instead of trying to insert a user and acting on the error I should be checking if the user exists first in the user context function and if it does just return that user object and don’t try to insert anything. So to anyone using this new get_or_insert function they can assume that any error indicates that something bad happened instead of having to make sense of the error.

Thanks in advance for the help!

Showing Posts 1 to 5

dimitarvp

dimitarvp

I usually do almost the same as you here, with the exception of making a function to search for a specific error in the list of changeset errors:

defp email_taken?({:email, {_, [constraint: :unique, constraint_name: _]}}), do: true
defp email_taken?(_), do: false

def do_stuff() do
  cs = insert_stuff()
  email_is_taken = Enum.any?(cs.errors, &email_taken?/1)
end

The advantage of this approach is that you can also check for other errors, the code looks a bit cleaner and more readable (IMO), and the whole thing is composable and the approach can be reused (you just need to add new checker functions).

Francesco

Francesco OP

I like how you have abstracted that functionality, definitely cleaner.

I’m currently looking to avoid the situation all together though and use upserts to my advantage so I can just pretend it was inserted and role with it. Having a few issues getting upserts to work with unique partial indexes though…

dimitarvp

dimitarvp

I am not saying you should not chase upserts but there should be nothing stopping you just putting an unique constraint both in the DB and in the Ecto schema and just catch that, and react to it.

Don’t spend too much time on such minutiae. Put a TODO somewhere after you make it work in a lame manner (namely like I suggested :003:) and just move on.

stefanchrobot

stefanchrobot

I’m doing it this way:

  def create_changeset(...) do
    ...
    |> unique_constraint(:some_id, message: "already_taken")
  end

  # in the same module
  def already_taken?(%Ecto.Changeset{} = changeset) do
    case changeset.errors[:some_id] do
      {"already_taken", _} -> true
      _ -> false
    end
  end

I would advise against doing that, since you’re defeating the purpose of RDBMS-based validations that Ecto embraces. If you were to choose that approach, you’d still need to keep the code for handling failed constraint to make sure nobody inserts the user between your SELECT and INSERT, so you’re back where you started.

thojanssens1

thojanssens1

Unfortunately that code logic won’t work if you wanna use it in a transaction.

** (Postgrex.Error) ERROR 25P02 (in_failed_sql_transaction) current transaction is aborted, commands ignored until end of transaction block

Looks like once you got a constraint error, the transaction is aborted. Seems like you have to call get, and if nil then create the record. It’s less safe if another process created the record between the get and create call, but I don’t see any easy safer way.

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
mnkhod
So i have been using ash framework for a while and i love it. However currently the issue im having with ash framework is the error handl...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews