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

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews